본문 바로가기
Java/트러블 슈팅

Jenkins으로 CI/CD 구축하기

by oneny 2023. 12. 9.

Jenkins으로 CI/CD 구축하기

다른 분들과 협업 프로젝트를 진행하면서 요구사항은 계속 바뀔 수 있기 마련이다. 하지만 폭포수 모델처럼 분석, 설계 영역에 상당한 시간이 할당되고, 해당 작업이 끝난 후에는 개발 시 어떤 추가나 수정도 허용되지 않으면서 전체 개발이 끝난 후에 통합이 진행된다면 그 결과로 수 많은 통합 이슈가 가장 마지막 단계에서 발생하는 문제가 발생할 수 있다.

따라서 현재 진행하는 이커머스 프로젝트는 충분히 잦은 요구사항이 발생할 수 있는 서비스이기 때문에 애자일처럼 빠르고 유연하며 조금씩 발전되는 소프트웨어 개발을 통해 목표를 계속 수정해나가는 방법론을 선택했다. 따라서 하나 혹은 그 이상의 기능을 구현하고 테스트, 빌드, 배포하는 것이 자주 발생하기 때문에 이러한 반복적인 개발 사이클과 빈번한 릴리스를 CI/CD를 통해서 해결하려고 한다.

 

지속적 통합(CI, Continuous Integration)

지속적 통합은 개발자들이 빠른 주기로 작업한 내용을 통합 브랜치에 통합하고 빌드하는 프로세스를 의미한다. 통합은 개인이 작업한 코드를 공용 작업 환경에 올리는 것으로 이 과정은 개인 기능 브랜치를 통합 브랜치에 병합하는 과정으로 이뤄진다. 간단히 요약하자면 빌드/테스트  자동화 과정이라고 할 수 있다.

CI는 통합하는 과정에서 발생하는 이슈를 가능한 빨리 발견하기 위해 필요하다. 기능을 개발할 때는 코드를 여러 번 수정하게 되는데, 이 과정에서 코드를 반영하고, 버전 관리 시스템에서 변경 사항을 가져오고, 소스코드를 빌드하고, 단위 테스트를 진행하고, 통합하고, 통합된 코드를 빌드하고, 이를 묶어 배포하는 등 여러 과정을 수행한다. CI 환경에서는 젠킨스 같은 도구를 이용해 모든 과정을 빠르게 에러 없이 진행할 수 있다.

 

지속적 전달/배포(CD, Continuous Delivery / Continuous Deployment)

지속적 전달은 지속적 통합 이후에 더 나아가 통합된 코드를 자동으로 테스트하고, 테스트를 통과한 코드를 프로덕션 환경에 안전하게 전달할 수 있는 프로세스를 의미한다. 간단히 요약하자면 배포 자동화 과정이라고 할 수 있다.

이를 통해 개발된 최신 버전의 어플리케이션을 언제든지 운영 환경에 배포할 수 있는 상태를 유지할 수 있고, 사용자에게 빠르게 신규 기능을 제공하면서 버그 수정과 보완 사항을 신속하게 적용할 수 있다.

 

브랜치 전략

프로젝트를 진행하면서 다음과 같이 크게 세 개의 브랜치 전략을 사용했다.

  • 마스터 브랜치(main): 배포 가능한 상태만을 관리하는 브랜치로 이 브랜치에는 이미 배포됐고, 모든 테스트를 완료한 코드가 있다. 어떤 개발도 이 브랜치에서 작업되지 않는다.
  • 통합 브랜치(develop): 다음 배포할 것을 개발하는 브랜치로 통합 이슈를 발견하기 위해 여기에서 모든 기능이 통합, 테스트, 빌드된다. 이 브랜치에서도 어떤 개발도 진행되지 않고, 개발자가 이 브랜치에서 기능 브랜치를 따서 작업할 수 있다.
  • 기능 브랜치(feature): 실제 개발이 일어나는 브랜치다. 통합 브랜치에서 파생된 여러 기능 브랜치가 존재할 수 있다. 기능 브랜치나 통합 브랜치에 커밋된 내용(머지도 커밋을 생성)은 빌드, 정적 코드 분석 및 테스트를 거치게 된다.

 

Jenkins를 이용한 CI

Jenkins CI 도구는 스크립트 문장을 사용하여 선언전 파이프라인(declarative pipeline)을 생성하는 방법을 제공한다. 각 파이프라인에는 고유한 목적이 있는데 CI를 관리하거나, 소작업을 관리하거나, 배포를 진행한다. 파이프라인은 연속된 기술적인 작업의 흐름이고, 각 작업은 연속해서 수행되는 소작업의 모음이라고 할 수 있다.

develop 브랜치로 푸시되는 코드들의 테스트, 빌드의 자동화 작업을 진행해보자.

 

Jenkins 서버 생성하고 Jenkins 설치

먼저 네이버 클라우드 플랫폼에서 회원가입하면 10만원까지 크레딧을 지원해주기 때문에 서버 구축에 필요한 모든 인스턴스는 NCP를 이용하려고 한다. NCP에서 제공하는 Jenkins Server 상품을 이용하면 더 쉽게 구축할 수 있지만, 직접 인스턴스를 생성해 Jenkins 서버를 구축해 보는 연습을 하기로 했다. 개인 사이드 프로젝트이기 때문에 월 35,000원의 [Compact] 1vCPU, 2GB Mem, 50GB Disk [g1] 서버를 생성했다.

 

 

How To Install Jenkins on Ubuntu 18.04 | DigitalOcean

 

www.digitalocean.com

Jenkins 설치는 위를 참고하여 설치를 진행했다.

 

Jenkins 시작

초기 비밀번호를 획득하여 들어가면 Jenkins가 추천하는 plugin을 설치해서 Jenkins amdin 계정을 만든 후에 기본 설치를 완료할 수 있다. 그리고 젠킨스 파이프라인을 생성하기 전에 Github에서 자신의 계정의 access token과 프로젝트 Repository의 Web hook을 생성해야 한다.

 

Github Access Token 발급

Github > Setting > Developer settings > Personal access token > Generate new token으로 이동해 토큰을 등록한다. 적당한 이름을 지어주고 repo, admin:repo_hook 체크를 해준 후 토큰을 생성한다. 

 

Webhooks 설정

그 다음은 프로젝트의 Webhook을 설정해줘야 한다. 프로젝트 Repository > Settings > Webhooks > Add Webhook을 클릭하면 위와 같은 화면이 나오는데 Payload URL은 "(젠킨스 서버)/github-webhook/"을 입력해야 한다. Content type으로는 application/json으로 설정한다. 다시 젠킨스 서버로 돌아가서 위에서 생성한 access token으로 Credentials를 생성하자.

Jenkins Credentials 생성

생성된 토큰 값을 복사해 Jenkins 관리 > Credentials 클릭해서 위 사진에서 보이는 Domains 밑 (global)를 클릭한다.

 

 

 

Add Credentials를 클릭한 후 위 사진에서 볼 수 있듯이 Username은 Github ID를 적어주고, Password는 위에서 생성한 Github Personal Access Token을 입력해준다. 그리고 ID 필드는 식별 가능한 자격 증명 ID를 입력하면 된다. 다 입력해주고 Create 버튼을 클릭해서 생성하자.

 

스크립트 작성

Jenkins 파이프라인을 생성하자. 파이프라인을 통해 스크립트 파일(JenkinsFile)을 작성해 CI&CD를 위한 연속적인 이벤트를 실행시킬 수 있다. 스크립트를 설정하는 방법에는 다음과 같은 두 가지 방법이 있다.

  • Pipeline Script(default): 젠킨스 웹 내에서 스크립트를 작성하여 관리
  • Pipeline Script from SCM: 프로젝트 내에서 Jenkinsfile에 스크립트를 작성하여 관리

 

Pipeline Script를 사용한 파이프라인 구축

 

먼저 1번 방법을 사용하여 파이프라인 구축을 해보자.

pipeline {
    agent any

    stages {
        stage('Git Clone') {
            steps {
                git branch: 'develop', url: 'https://github.com/f-lab-edu/i-dol-u.git'
            }
        }
        
        stage('Test') {
            steps {
                sh './gradlew test'
                echo 'test success'
            }
        }

        stage('Build') {
            steps {
                sh './gradlew clean build'
                echo 'build success'
            }
        }
    }
}

 

CI/CD 젠킨스 파이프라인을 구성하려면 파이프라인 문법을 작성해야 한다. 따라서 간단히 먼저 작성한 CI 스크립트에 대해서 살펴보자.

  • pipeline { ... }: 가장 먼저 pipeline 키워드를 선언한 뒤 이 틀안에서 파이프라인을 작성하겠다는 것을 의미한다.
  • agent any: agent는 파이프라인 블록 내 최상단에 정의해야 하는데 파이프라인 혹은 스테이지를 실행하기 위해 사용할 노드를 지정한다. 이 때 agent any는 해당 파이프라인이나 스테이지에 상관없이 어떠한 agent에서도 실행될 수 있다는 것을 알려준다.
  • stage('Git Clone') { ... }: Git Clone 단계에서 Git에 올라가 있는 프로젝트의 develop 브랜치를 클론하도록 했다.
  • stage('Test') { ... }: Test 단계에서는 테스트를 진행하도록 작성했다.
  • stage('Build'): 해당 단계를 통해 클론한 프로젝트를 빌드를 통한 CI 작업을 진행하도록 했다.

 

생성한 파이프라인으로 들어가 좌측에 있는 메뉴바의 Build 버튼을 클릭하게 되면 위처럼 Git Clone부터 Build까지 CI 과정이 정상적으로 동작하는 것을 확인할 수 있다.

 

Pipeline Script for SCM를 사용한 파이프라인 구축

 

이번에는 Pipeline Script for SCM를 사용하여 파이프라인을 구축해보자. SCM은 git이나 svn과 같은 소스관리 도구를 의미하는데 즉, pipeline script를 소스상에서 관리하겠다는 것이다. 위에서 Github project를 클릭하고 Project url을 branch 부분 없이 project url를 입력한다. 그리고 GitHub hook trigger for GITScm polling을 클릭한다.

 

Definition에서 Pipeline script form SCM을 선택한다. SCM은 GIT으로 선택하고, Repository URL를 입력하고 Credentials는 위에서 생성한 것을 선택하면 된다. Branch Specifier에서 ***job를 빌드할 브랜치를 선택하고 Github Repository에서 Pipeline Script가 있는 Path를 Script Path에 입력하면 된다. 이제 Pipeline script를 Github repository에 생성하자.

 

pipeline {
    agent any

    stages {
        stage('Git Checkout') {
            steps {
              checkout scm
              echo 'Git Checkout Success!'
            }
        }

        stage('Test') {
            steps {
                sh './gradlew test'
                echo 'test success'
            }
        }

        stage('Build') {
            steps {
                sh './gradlew clean build'
                echo 'build success'
            }
        }
    }
}

처음에 작성한 스크립트와 차이가 있다면 Git Clone 단계를 Git Checkout 단계로 수정했다. checkout scm 명령은 해당 SCM(Source Code Management) 설정을 사용하여 소스 코드를 가져오는 것을 의미한다. 즉, 위에서 Repository URL을 설정한 저장소의 최신 코드를 Jenkins 워크스페이스로 복제하는 것이다.

 

PR를 통해 develop 브랜치로 통합하면 위에서 설정한 Webhook으로 Jenkins 서버에서 통합하고 빌드하는 것을 확인할 수 있다!

 

 

Jenkins를 이용한 CD

이제 main 브랜치로 푸시되는 코드들이 운영 환경에 배포되는 과정을 자동화해보자.

 

추가 plugin(Publish Over SSH) 설치

만약 여러 원격 서버에 파일을 배포하고 빌드 스크립트를 실행하기 위해서는 Publish Over SSH plugin을 설치해야 한다. 위 사진에서도 볼 수 있듯이 Available plugins는 Dashboard > Jenkins 관리 > Plugins에서 확인할 수 있다.

 

 

Publish Over SSH 설정

Jenkins 관리 > System > Publish Over SSH 탭으로 가서 ssh 접속 키인 pem 파일 내용을 입력한다.

 

SSH Server 추가 시 설정해야할 정보들을 다음과 같다.

  • Name: 임의의 서버 이름
  • Hostname: 실제로 접속할 원격 서버 ip
  • Username: 접속할 원격 서버의 user 이름
  • Remote Directory: 접속 후 이동할 디렉터리
  • Use password authentication, or use a different key: 체크박스를 선택하고 접속할 때 필요한 비밀번호를 입력하고 저장을 눌러 정보를 저장한다.

 

CD를 위한 파이프라인 생성

pipeline {
    agent any

    stages {
        stage('Git Checkout') {
            steps {
              checkout scm
              echo 'Git Checkout Success!'
            }
        }
        
        stage('Test') {
            steps {
                sh './gradlew test'
                echo 'test success'
            }
        }

        stage('Build') {
            steps {
                sh './gradlew clean build'
                echo 'build success'
            }
        }
        
        stage('Deploy') {
            steps([$class: 'BapSshPromotionPublishPlugin']) {
                sshPublisher(
                    continueOnError: false,
                    failOnError: true,
                    publishers: [
                        sshPublisherDesc(
                            configName: 'server1',
                            verbose: true,
                            transfers: [
                                sshTransfer(
                                    sourceFiles: "build/libs/*.jar",
                                    removePrefix: "build/libs",
                                    remoteDirectory: "app",
                                    execCommand: "sh ~/app/deploy.sh"
                                )    
                            ]
                        )
                    ]
                )
            }
        }
    }
}

파이프라인 생성은 위에서 했던 것과 똑같이 진행하고, 이 때 Deploy 단계를 추가했다. 빌드된 jar 파일을 전송하면 운영 서버에서 deploy.sh 스크립트를 실행한다. Deploy 단계에 대해 세부적으로 살펴보면 다음과 같다. 

  • continueOnError: 파이프라인의 현재 단계에서 에러가 발생했을 때, 그 에러가 무시되고 파이프라인이 계속 진행되도록 하는 옵션
  • failOnError: 파이프라인의 현재 단계에서 오류가 발생하면 파이프라인이 실패로 표시되도록 하는 옵션
  • sshPublisher: SSH로 발행(publish)하는데 필요한 설정을 지정한다.
  • configName: Jenkins의 시스템 설정에서 미리 구성된 SSH 서버 정보를 사용하기 위한 구성 이름
  • verbose true: SSH 실행을 자세하게 로깅
  • transfers: 파일 전송 구성
    • sshTransfer: SSH를 통해 파일을 전송하는 작업
      • sourceFiles: "build/libs/*.jar": 전송할 파일의 경로 지정
      • removePrefix: "build/libs": 전송할 파일에서 제거할 경로 지정
      • remoteDirectory: "app": 원격 서버의 어떤 디렉터리에 파일을 전송할지 지정
      • execCommand: "sh ~/app/deploy.sh": 파일 전송 후에 실행할 명령을 지정

 

deploy.sh 작성

REPOSITORY=/root/app
PROJECT_NAME=$(ls /root/app/ | grep SNAPSHOT.jar | head -1)
CURRENT_PID=$(pgrep -f ${PROJECT_NAME})

echo "> 현재 구동중인 애플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
	echo ">현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
	echo "> kill -15 $CURRENT_PID"
	kill -15 $CURRENT_PID
	sleep 3
fi

echo "> 새 애플리케이션 배포"

nohup java -jar $REPOSITORY/$PROJECT_NAME >> $REPOSITORY/nohup.out &

원격 서버에 접속해서 실행된 쉘 스크립트(Shell script) 파일을 작성한다. pgrep 명령어를 통해 현재 PROJECT_NAME(해당 디렉터리에서 SNAPSHOT.jar로 끝나는 첫 번째 파일의 이름)를 사용하여 현재 실행 중인 프로세스의 PID를 찾고 만약 현재 실행 중인 프로세스가 없다면 없다는 메시지를 출력한다. 만약 실행 중인 프로세스가 있다면 kill -15 명령을 사용하여 프로세스를 종료하고, 3초 동안 대기한다.

그리고 nohup 명령어를 통해 터미널이나 SSH 세션을 종료해도 프로세스가 계속 실행되도록 하고, jar -jar $REPOSITORY/$PROJECT_NAME으로 새로운 JAR 파일을 백그라운드에서 실행하도록 한다. 그리고 실행시킨 로그를 nohup.out 파일에 추가한다.

 

배포 자동화 진행

만약 구동중인 애플리케이션이 있다면 해당 애플리케이션을 종료한 후 재시작하도록 배포 자동화 과정까지 모두 성공한 것을 확인할 수 있다!

 

 

 

 

출처

[Jenkins] # 선언적(Declarative) 파이프라인

[DevOps] Jenkins를 통한 CI/CD 구축기 2편 (Backend CI/CD 구축)