사이먼's 코딩노트

[SpringBoot] 실전 서비스 배포 (4) 본문

Java/SpringBoot

[SpringBoot] 실전 서비스 배포 (4)

simonpark817 2024. 7. 30. 22:57

[실전 서비스 배포]

  • 저번 '실전 서비스 배포 (3)' 까지 진행하면서 docker를 통한 이미지 생성과 함께 실제 구매한 도메인 네임으로 접속까지 성공하는 실습을 진행했다.
  • 이번에는 실전 서비스 배포의 마지막 단계로 젠킨스를 통해 서비스 배포 자동화를 구현해봅시다.

 

[젠킨스(Jenkins)란?]

  • 젠킨스는 지속적 통합(Continuous Integration, CI) 및 지속적 전달(Continuous Delivery, CD)를 지원하는 오픈 소스 자동화 도구이다.
  • 젠킨스는 개발자들이 소프트웨어 개발 과정에서 반복적인 작업들을 자동화하여 개발 생산성과 품질을 향상시킬 수 있도록 도와준다.
  • 젠킨스는 다양한 프로그래밍 언어와 프레임워크를 지원하며, 소스 코드 컴파일, 테스트 실행, 정적 분석, 배포 등의 작업을 자동화할 수 있다.
  • 개발자가 Git이나 Subversion 등에 커밋을 하면, 젠킨스는 이를 감지하고 지정된 작업들을 자동으로 실행한다.
  • 젠킨스는 사용자가 웹 인터페이스를 통해 다양한 작업을 구성하고 관리할 수 있다.
  • 이 작업들은 빌드 단계, 테스트 단계, 배포 단계 등으로 구성될 수 있으며, 개발자들은 작업 간의 의존성과 실행 조건을 설정할 수 있다.
  • 예를 들어, 새로운 코드 커밋이 있을 때마다 자동으로 빌드하고 테스트를 실행하여 결과를 확인할 수 있다.
  • 젠킨스는 다양한 플러그인을 지원하여 기능을 확장할 수 있으며, 이를 통해 젠킨스는 다양한 도구와 시스템과의 통합이 가능하며, 사용자의 요구에 맞게 유연하게 구성할 수 있다.
  • CI/CD의 원칙을 따라 개발 프로세스를 자동화하고 효율적으로 관리할 수 잇는 젠킨스는 개발자 사이에서 높은 인기를 얻고있다.

 

[젠킨스를 사용한 간단한 예시]

  • 코드 빌드 : 개발자가 코드를 커밋하면, 젠킨스는 해당 코드를 자동으로 가져와 빌드한다. 이를 통해 코드 빌드 단계에서 발생할 수 있는 문제를 빠르게 감지하고 수정할 수 있다.
  • 테스트 자동화 : 빌드된 코드에 대해 자동화된 테스트를 실행하여 버그와 결함을 식별한다. 예를 들어, 단위 테스트, 통합 테스트, 성능 테스트 등을 자동으로 실행하여 코드 변경 사항의 영향을 신속하게 확인할 수 있다.
  • 배포 자동화 : 코드가 테스트를 통과하면 젠킨스는 자동으로 배포를 수행할 수 있다. 이를 통해 개발된 소프트웨어를 실제 서버나 클라우드 환경으로 자동으로 배포할 수 있다. 예를 들어, 서버에 업데이트된 애플리케이션을 배포하거나, 클라우드 서비스에 새로운 가상 머신을 프로비저닝할 수 있다.
  • 모니터링 및 로깅 : 젠킨스는 배포된 애플리케이션의 모니터링과 로깅을 관리할 수 있다. 예를 들어, 애플리케이션의 성능 지표를 모니터링하고 로그를 수집하여 이슈를 신속하게 파악하고 해결할 수 있다.

 

[젠킨스 설치]

  • 먼저 docker를 통해 젠킨스를 설치해야한다.
  • 아래 명령어를 SSH 클라이언트에 입력하여 젠킨스를 설치하고, 설치하기 전 먼저 기존에 젠킨스 docker 컨테이너가 있으면 삭제를 한다.
# 기존 컨테이너 삭제, 기존 디렉토리 삭제
docker rm -f jenkins_1
rm -rf /docker_projects/jenkins_1

# 젠킨스 설치
docker run \
    --name jenkins_1 \
    -p 8081:8080 \
    -e TZ=Asia/Seoul \
    -v /docker_projects/jenkins_1/var/jenkins_home:/var/jenkins_home \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /docker_projects/jenkins_1/data:/data \
    -u root \
    -d \
    --restart unless-stopped \
    jenkins/jenkins:jdk17

 

  • 설치를 마치면 npm으로 접속해서 proxy처리를 해줘야한다.
  • proxy 처리는 보안 또는 캐싱 및 성능 향상을 위해서 설정한다고 생각하면 좋다.
  • 젠킨스 뿐만 아니라 실제 배포될 서버의 도메인 네임과 npm 주소도 함께 등록해야한다.
  • 웹 사이트의 포트는 8080, npm의 포트는 81, 젠킨스의 포트는 8081로 추가를 해주고, 이렇게 설정을 하고 나면 기존의 보안이 떨어지는 http가 아니라 https로 접속이 가능하게 된다.
  • 여기서 172.17.0.1 IP는 클라우드 플랫폼에서 VPC 생성 시 IP 주소 범위 중 하나를 택한 것이다.

npm proxy 처리

 

  • 다음은 젠킨스를 진입했을 때 필요한 초기 비밀번호를 확인하기 위해 SSH 터미널에 아래와 같은 명령어를 입력한다.
  • 명령어를 입력한 후 비밀번호를 확인했다면, https://jenkins.도메인명을 입력하면 젠킨스에 접속하게 되고 초기 비밀번호를 입력하면 계정 정보를 변경할 수 있다.
  • 계정을 수정하면 메인 메뉴 좌측의 새로운 Item을 선택하여 아이템을 생성한다. 그리고 이 때 Item type은 pipeline을 선택해야한다.
# 젠킨스 초기 비밀번호 확인
docker exec jenkins_1 cat /var/jenkins_home/secrets/initialAdminPassword

젠킨스 계정 정보 변경
젠킨스 프로젝트 생성

 

[깃허브 리포지터리 웹훅 추가]

  • 이 다음으로는 실제 배포될 프로젝트의 깃허브 리포지터리와 젠킨스를 연결시켜 main에 push가 일어날 때마다 알림이 갈 수 있도록 설정을 해야한다.
  • 아래 사진과 같이 깃허브와 젠킨스에서 모두 웹훅에 의한 호출을 허용하도록 설정해야한다.

깃허브 웹훅 설정

 

젠킨스 웹훅 설정

 

[젠킨스 내 Docker 설치]

  • 다음은 생성된 jenkins에 도커를 설치하여 HOST OS의 도커 명령어를 사용할 수 있도록 해야한다.
  • 아래 명령어를 통해 jenkins bash에 접속하여 도커를 설치해봅시다.
# jenkins_1 bash 접속
docker exec -it jenkins_1 bash

# jenkins_1 bash 도커 설치
apt-get update -y
apt-get install -y ca-certificates curl gnupg lsb-release
mkdir -p /etc/apt/keyrings
rm /etc/apt/keyrings/docker.gpg
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli docker-compose-plugin
exit

 

[파이프라인 작성]

  • 마지막으로 자동화를 구현하기 위해선 해당 순서를 정리하여 젠킨스 파이프라인에 작성을 해야한다.
  • 파이프라인에 담겨질 내용은 간단하게 말하면 우리의 '실전 서비스 배포 (3)' 에서 진행했던 도커 이미지 삭제 후 새로 커밋한 프로젝트 pull 하기, 새로운 도커 이미지 생성과 같은 작업들을 작성한다.
  • 프로젝트 내에는 Dockerfile이 존재해야되고, 여기서 말하는 Dockerfile 파일의 내용은 '실전 서비스 배포 (3)'에서 생성한 내용과 동일하게 작성하면된다.
  • 아래 사진과 같이 젠킨스에서 구성으로 들어가 파이프라인을 작성하면 되고, 파이프라인 내용도 함께 참고 부탁드립니다.
  • 작성된 파이프라인 내에서 market이라고 되어있는 부분은 Docker 이미지의 이름이기 때문에 사용자 임의로 변경해도상관없지만, 주의할 점은 본인 프로젝트의 깃허브 리포지터리 주소를 입력하는 것과, application-secret.yml의 내용 모두를 작성해야한다는 것이다.
pipeline {
    agent any

    environment {
        timestamp = "${System.currentTimeMillis() / 1000L}"
    }

    stages {
        stage('Prepare') {
            steps {
                script {
                    // Get the ID of the market:latest image
                    def oldImageId = sh(script: "docker images market:latest -q", returnStdout: true).trim()
                    env.oldImageId = oldImageId
                }

                git branch: 'main',
                        url: '깃허브 리포지터리 주소 입력'
            }

            post {
                success {
                    sh 'echo "Successfully Cloned Repository"'
                }
                failure {
                    sh 'echo "Fail Cloned Repository"'
                }
            }
        }

        stage('Build Gradle') {
            steps {
                dir('.') {
                    sh """
                    cat <<EOF > src/main/resources/application-secret.yml     -> application-secret.yml 내용이 들어가야됨
                        spring:
                          security:
                            oauth2:
                              client:
                                registration:
                                  kakao:
                                    clientId: 
                    """
                }
                dir('.') {
                    sh """
                    chmod +x gradlew
                    """
                }

                dir('.') {
                    sh """
                    ./gradlew clean build -x test
                    """
                }
            }

            post {
                success {
                    sh 'echo "Successfully Build Gradle Test"'
                }
                failure {
                    sh 'echo "Fail Build Gradle Test"'
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                script {
                    sh "docker build -t market:${timestamp} ."
                }
            }
        }

        stage('Run Docker Container') {
            steps {
                script {
                    // Check if the container is already running
                    def isRunning = sh(script: "docker ps -q -f name=market_1", returnStdout: true).trim()

                    if (isRunning) {
                        sh "docker rm -f market_1"
                    }

                    // Run the new container
                    try {
                        sh """
                        docker run \
                          --name=market_1 \
                          -p 8080:8080 \
                          -v /docker_projects/market_1/volumes/gen:/gen \
                          --restart unless-stopped \
                          -e TZ=Asia/Seoul \
                          -d \
                          market:${timestamp}
                        """
                    } catch (Exception e) {
                        // If the container failed to run, remove it and the image
                        isRunning = sh(script: "docker ps -q -f name=market_1", returnStdout: true).trim()

                        if (isRunning) {
                            sh "docker rm -f market_1"
                        }

                        def imageExists = sh(script: "docker images -q market:${timestamp}", returnStdout: true).trim()

                        if (imageExists) {
                            sh "docker rmi market:${timestamp}"
                        }

                        error("Failed to run the Docker container.")
                    }

                    // If there's an existing 'latest' image, remove it
                    def latestExists = sh(script: "docker images -q market:latest", returnStdout: true).trim()

                    if (latestExists) {
                        sh "docker rmi market:latest"

                        if(!oldImageId.isEmpty()) {
                            sh "docker rmi ${oldImageId}"
                        }
                    }

                    // Tag the new image as 'latest'
                    sh "docker tag market:${env.timestamp} market:latest"
                }
            }
        }
    }
}

파이프라인 작성

 

  • 이렇게 파이프라인까지 작성됐다면, 실제로 배포중인 프로젝트에 main 브랜치에서 git push를 하게 되면 push와 동시에 jenkins에서 자동으로 빌드가 진행되고 배포가 진행된다.
  • 아래 사진과 같이 좌측 하단에 빌드 성공 여부가 나타나는 것을 확인할 수 있다.

빌드 성공

 

 

반응형