Spring boot 프로젝트 Jenkins CI/CD 구축기 (with Gitea)

BK·2024년 11월 26일
0

목적 : Jenkins를 이용하여 Spring Boot 프로젝트 빌드/배포 자동화하기

회사에서 스프링부트로 개발한 웹 애플리케이션의 수동배포를 Jenkins를 도입하여 자동으로 빌드와 배포가 가능하도록 변경한 내용입니다.

1. Jenkins 도입 전후 비교

기존 애플리케이션 배포 방식

  1. 개발자 로컬 환경 PC에서 IntelliJ를 이용하여 jar 파일을 빌드
  2. FileZilla를 이용하여 각 DEV, PROD 서버에 빌드파일 FTP 업로드
  3. 터미널 SSH를 이용하여 jar 파일 실행

젠킨스 배포 방식

  1. 개발 완료 후 Git에 Push
  2. 젠킨스 내부에서 gradle build 후 dev, main 브랜치 별로 서버에 파일 전송
  3. 파이프라인 스크립트를 통해 자동 파일 실행

2. Jenkins 설치

1) Docker 설치 (with Rocky Linux 8.10)

젠킨스를 설치하기 전에 컨테이너 관리 플랫폼인 docker를 우선적으로 설치한 후에 젠킨스 컨테이너를 설치한다.

(1) Linux Dnf 유틸 설치

dnf install -y dnf-utils

(2) Docker 레포지터리 추가

dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

(3) Docker 설치 (충돌 패키지 삭제)

dnf install -y docker-ce --allowerasing

(4) 설치 확인

docker --version
systemctl status docker

(5) 서비스 활성화 및 시작

systemctl enable docker
systemctl start docker

참고링크: Rocky Linux 8에 docker 설치하기

2) Portainer CE 설치

도커 컨테이너들을 GUI환경에서 관리하기 위해 Portainer 컨테이너를 설치한다.

(1) image pull Portainer-ce

portainer/portainer 로 할 경우 예전 버전을 내려받게 되어 있어서 뒤에 꼭 ce를 붙여주는게 좋다.

docker pull portainer/portainer-ce

(2) Volume 생성

docker volume create portainer_data

(3) Container 생성

docker run -d -p 9000:9000 --name portainer --restart always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce

(4) Portainer 접속

브라우저에서 서버아이피(ex 10.22.33.44)에 포트 9000을 붙여 접속한다

http://10.22.33.44:9000

처음 접근시 어드민 계정 생성을 위해 ID/PW를 입력해줘야 한다.
ID/PW는 기억해주자.

참고링크: Portainer 설치

3) Jenkins 설치

(1) image pull jenkins

접근한 Portainer 에서 image 페이지에서 jenkins/jenkins 입력후 pull the image 클릭 (특정 버전 기입이 없으면 자동으로 latest 버전으로 받아진다.)

(2) Volume 생성

Volumes 페이지로 이동하여 Add Volume을 누른 후 이름을 jenkins_volume으로 입력 후 Create the volume을 클릭한다

(3) Container 생성

  • Container 페이지로 이동하여 Add Container 클릭

  • Name에 jenkins, Image에 jenkins/jenkins:latest, 그리고 map additional port를 눌러 host 7602 container 8080(젠킨스는 무조건 8080으로 받음.필수) 입력

  • 컨테이너를 만들기 전 옵션 설정을 위해 화면 맨 하단 탭 중에 volumes 탭을 눌러 볼륨 매핑을 해준다. map additional volume을 눌러 매핑할 항목을 총 2개로 만들어주고, 아래 이미지처럼 작성한다.

    container : /var/jenkins_home
    volume : jenkins_volume
    container(bind) : /var/run/docker.sock
    host : /var/run/docker.sock

  • Env 탭으로 이동하여 타임존 환경변수 설정을 한다. add an environment variable 을 클릭하고 name TZ , value Asia/Seoul 입력

  • Restart Policy 탭으로 이동해서 unless stopped 선택

  • 상단에 Deploy the container 를 클릭하여 컨테이너 생성

(4) 젠킨스 접속

브라우저에서 서버아이피(ex 10.22.33.44)에 포트 7602를 붙여 접속한다

http://10.22.33.44:7602

젠킨스 생성 시 처음 발급받은 패스워드 키가 별도로 생성되므로 서버에서 하단 경로로 이동하여 vi편집기를 통해 생성된 패스워드를 확인하여 붙여넣는다.

패스워드 성공 후 어드민 계정까지 생성해 준다.

참고링크: Docker를 WebUI로 관리하기 - Jenkins 설치하기

3. Jenkins & Gitea 환경 설정

1) Jenkins 플러그인 설치

스크립트 진행에 필요한 플러그인들을 설치한다.
Jenkins 관리 > Plugins 로 이동하여 Available plugins에서 아래 항목을 검색하여 설치 진행

  • Multibranch Scan Webhook Trigger
  • Pipeline: Multibranch with defaults
  • Gitea Plugin
  • Publish Over SSH

2) Gitea 액세스 토큰 발급

(1) 사용자 설정으로 이동

우측 계정 정보에서 설정으로 이동

(2) 어플리케이션에서 액세스 토큰 발급

어플리케이션 페이지에서 신규 엑세스 토큰 발급. 발급 후 생성된 액세스토큰은 따로 기록해 놓기

(3) 젠킨스 사이트에서 액세스 토큰 저장

젠킨스 관리 > Credentials > System > Global credentials 에서 Add Credentials 클릭

Gitea Personal Access Token 을 선택하여 액세스 토큰 기입 및 id 와 설명란 입력

(4) gitea 연동 환경 설정

젠킨스 관리 > System 으로 이동하여 Gitea Servers 항목에 환경 정보 저장

3) 서버 ssh 접속 설정

젠킨스 관리 > System 으로 이동하여 Publish over SSH 항목에 환경 정보 저장

  • Name : 서버 정보를 호출할 때 사용할 명칭으로 저장 ex) root@DEVWEBSERVER
  • Hostname : 서버 IP 주소
  • Username : ssh 접속에 사용되는 계정 ex)root
  • Remote Directory : ssh 접속 후 시작 디렉토리
  • 고급 설정에서 Use password authentication, or use a different key 항목에 체크하여, 접속 계정의 비밀번호를 기입하여 넣고 포트를 설정해준 후 저장 한다.

이 때 개발/운영서버의 정보가 여러개일 경우, 여러개 다 저장해둔다. (개발계 서버, 운영계 서버 등)

4) Gradle 설정

스프링부트 프로젝트 gradle SDK 버전에 맞춰서 미리 빌드 설정을 잡아 준다.

  • name : 파이프라인에서 호출할 그래들 명칭
  • install automatically 후에 그래들 버전정보를 맞춰준다
  • 정보 저장 후 save 클릭

4. jenkins Pipeline Script 생성

1) Groovy file 생성

Jenkins 관리 > Managed files로 이동하여 Add a new Config 선택

  • Type : Groovy file
  • ID : 스크립트 호출 시 사용할 id 저장 ex) pipe-sample
  • next 선택

2) pipeline script 임시 생성

이름에 프로젝트 명을 넣고 임시 스크립트를 집어넣는다.

  • tool gradle : 아까 생성한 gradle name 넣기
  • git url : 사용중인 git 주소
  • git branch : dev / main
  • credentailsId : git에서 생성한 액세스 토큰을 저장한 id 정보
pipeline {
  agent any

  tools {
      gradle 'Gradle-8.2.1'
  }
  
  stages {
    
    stage('Clone Repository') {
      steps {
        script {
          if (env.BRANCH_NAME == 'dev') {
            git url: 'http://gitip:3000/appstore/appstore_bos.git', 
              branch: 'dev', 
              credentialsId: 'Jenkins Access Token'
          } else if (env.BRANCH_NAME == 'main') {
            git url: 'http://gitip:3000/appstore/appstore_bos.git', 
              branch: 'main', 
              credentialsId: 'Jenkins Access Token'
          } else {
            error("Unsupported branch: ${env.BRANCH_NAME}")
          }
        }
      }
    }
    
    stage('Build with Gradle') {
      steps {
        script {
          sh "gradle clean bootJar"
        }
      }
    }
  }
  
  post {
    success {
      echo 'Build and completed '
    }
    failure {
      echo 'Build failed. Please check the logs.'
    }
  }
  
}

이후 summit을 누른다

5. Multibranch Pipeline 작업

1) 새로운 Item

젠킨스 메인화면에서 new Item을 눌러 멀티브랜치 파이프라인을 생성해준다.

  • name : 프로젝트 명칭
  • type : 아까 플러그인 설치를 제대로 했으면 , Multibranch Pipeline with defaults 항목이 보일 것이다. 선택한 후 ok 클릭

2) 브랜치 소스 선택

  • Display Name : 프로젝트 명 입력
  • Description : 설명 입력
  • Branch Sources : gitea 선택
  • 아까 젠킨스 환경 설정이 잘되어 있다면, server 와 credentials 콤보박스에서 정상적으로 선택이 가능하다.

Owner에는 gitea에 있는 조직명(혹은 사용자명)을 입력하면 알아서 해당 조직에 속한 레포지터리 리스트를 콤보박스로 보여준다. 혹시 콤보박스 항목이 나오지 않는 경우, 조직명을 다시 확인해봐야 한다.

gitea에서 보면 조직명/레포지터리로 구성되어 있다.
ex) appstore/appstore_bos

3) Behaviours

Discover branches 항목을 All branches로 선택 후 나머지 항목들은 삭제 한다. 이후 add 를 눌러 Filter by name(with regular expression) 선택

Regular expression 에 파이프라인 스크립트를 수행하고 싶은 브랜치만 기입. ex) dev|main

4) Build Configuration

jenkinsfile 선택란에는 아까 생성한 Pipeline Script Id를 입력해준다.

5) webhook 설정

(1) webhook 등록

gitea에서 사용할 webhook을 등록해준다. 이때 물음표를 눌러보면 사용할 수 있는 url이 나온다.
ex) 젠킨스서버ip:7602/multibranch-webhook-trigger/invoke?token=sample-test

(2) gitea webhook 연동

gitea 로 이동하여 프로젝트 > 설정 > 웹훅으로 이동 후 우측 상단 Webhook 추가 클릭

  • 대상 url : 아까 추가한 webhook url 입력 ex) 젠킨스서버ip:7602/multibranch-webhook-trigger/invoke?token=sample-test
  • 이후 다른 항목 건드리지 않고 저장

6. Git dev 브랜치에 소스 푸시

1) 스프링부트 소스를 변경한 후에 dev 브랜치에 소스 커밋하여 스크립트 구동 테스트

아마 첫 구동시에는 파이프라인 스크립트 때문에 실패가 떨어질 것이다.

2) Script 허용

젠킨스 관리 > In-process Script Approval로 이동

groovy script 를 Approve 해줘야 한다.
(스크립트가 변경될 때마다 계속 Approve를 해줘야한다. 따로 groovy 허용 없이 하는 방법이 있다고는 하는데 나중에 찾아봐야 함.)

3) 동작 실패한 빌드 재수행

dev 목록의 맨 우측의 플레이버튼 클릭

4) 정상 수행 확인

이후 해당 빌드 넘버를 클릭하여 Pipeline Overview 항목을 보면 파이프라인 스크립트가 정상 수행되었음 확인이 가능하다.

7. 파이프라인 스크립트 수정

1) Manage Files

젠킨스 관리 > Manage files로 이동 후 스크립트 Edit 진행.

  • git url : 깃 주소

  • git branch : 클론받을 브랜치 정보 ex) dev, main

  • git credentialsId : jenkins에 정의해놓은 gitea 액세스 토큰 아이디

  • jarFile : gradle build 후 jar 파일 경로/정보 (보통 build/libs 폴더에 위치하고 파일명은 프로젝트 명)

  • sshPublisher sshPublisherDesc configName : jenkins에 정의해놓은 ssh 통신 id

  • sshPublisher sshTransfer remoteDirectory : sftp 로 파일을 저장할 서버 경로

  • sshPublisher sshTransfer execCommand : 파일 전송 완료 후 실행시킬 커맨드 (서버에 미리 shell script 구현 후 해당 쉘스크립트를 호출)
    일반적으로 스프링부트 jar 파일 실행 스크립트를 구현해놓는다.

	nohup java -jar 파일경로 환경변수 &

2) 전체 스크립트 내용

pipeline {
    agent any

    tools {
        gradle 'Gradle-8.2.1'
    }

    stages {

        stage('Clone Repository') {
            steps {
                script {
                    if (env.BRANCH_NAME == 'dev') {
                        git url: 'http://gitip:3000/appstore/appstore_bos.git', 
                            branch: 'dev', 
                            credentialsId: 'Jenkins Access Token'
                    } else if (env.BRANCH_NAME == 'main') {
                        git url: 'http://gitip:3000/appstore/appstore_bos.git', 
                            branch: 'main', 
                            credentialsId: 'Jenkins Access Token'
                    } else {
                        error("Unsupported branch: ${env.BRANCH_NAME}")
                    }
                }
            }
        }
      
        stage('Build with Gradle') {
            steps {
                script {
                    sh "gradle clean bootJar"
                }
            }
        }

        stage('Transfer JAR to Remote Server and Reboot Program') {
            steps {
                script {
                    def jarFile = sh(script: "find build/libs -name 'appstore_bos.jar'", returnStdout: true).trim()
                    if (jarFile) {
                        if (env.BRANCH_NAME == 'dev') {
                            sshPublisher(
                                publishers: [
                                    sshPublisherDesc(
                                        configName: 'root@DEVWEBSERVER',
                                        transfers: [
                                            sshTransfer(
                                                sourceFiles: jarFile,
                                                remoteDirectory: "/usr/local/appstore",
                                                removePrefix: "build/libs",
                                                execCommand: """
                                                    sh /usr/local/appstore/runstore_bos.sh
                                                """
                                            )
                                        ],
                                        usePromotionTimestamp: false,
                                        verbose: true
                                    )
                                ]
                            )
                        } else if (env.BRANCH_NAME == 'main') {
                            sshPublisher(
                                publishers: [
                                    sshPublisherDesc(
                                        configName: 'root@PRODWEBSERVER',
                                        transfers: [
                                            sshTransfer(
                                                sourceFiles: jarFile,
                                                remoteDirectory: "/usr/local/appstore",
                                                removePrefix: "build/libs",
                                                execCommand: """
                                                    sh /usr/local/appstore/runstore_bos.sh
                                                """
                                            )
                                        ],
                                        usePromotionTimestamp: false,
                                        verbose: true
                                    )
                                ]
                            )
                        } else {
                            error("Unsupported branch: ${env.BRANCH_NAME}")
                        }
                    } else {
                        error("JAR file not found")
                    }
                }
            }
        }
    }

    post {
        success {
            echo 'Build and deployment completed successfully.'
        }
        failure {
            echo 'Build failed. Please check the logs.'
        }
    }
}

파이프라인 스크립트는 브랜치 전략 혹은 빌드/배포 정책에 맞게 수정하여 사용한다. 위 내용은 브랜치 별로 각각의 서버에 빌드 파일 전송 후 각 서버에 저장한 shell script를 통해 애플리케이션을 배포하는 방식으로 진행하였다.

8. 빌드 확인

git push를 통해 최종적으로 빌드가 정상적으로 동작하는지 확인한다.

profile
k-힙합을 사랑하는 개발자

0개의 댓글