본문 바로가기
[DevOps]/CI & CD

로컬에서 젠킨스로 블루/그린 무중단 배포 테스트하기 (3) - 젠킨스 파일 작성, 배포 테스트 진행

by 황원용 2023. 11. 27.
728x90
💡 이 글은 3편으로 2편인https://suzuworld.tistory.com/418에 이어 쓰는 글이다.
💡 1편부터 보려면 이곳으로 https://suzuworld.tistory.com/419

 

📌 JenkinsFile 작성 준비

  • 이제 젠킨스에 로그인해 보자.
  • 기존에 깃허브 아이디에 대한 Credential 등록은 이미 했다고 가정한다.
  • 잘 모르겠다면, 여기를 보고 따라 하면 된다. 매우 자세히 적어 놓았다.
  • 새로운 Item을 클릭한다.
    • Item은 Jenkins에서 관리되는 모든 항목을 의미한다. 일반적으로 프로젝트, 작업, 빌드, 파이프라인, 뷰 등이 있다.

 

  • 여기서 Pipeline을 선택하고 item의 이름을 기입한다.
    • 나는 jenkinTest라고 지었다. 이는 jenkinsFile을 작성할 때 스크립트 내부의 현재 경로가 된다.
      • /workspace/jenkinsTest가 기본 경로가 될 것이다.
    • jenkins를 설치하면 일반적으로 jenkins_home이라는 폴더가 디렉터리 어딘가에 생성되지만, m2 맥북에어로 테스트 시 /Users/<user명>/.jenkins 경로에 생성되었다.

 

  • Pipeline은 FreeStyle보다 훨씬 많은 자유도를 가지고 있으나 스크립트 파일을 직접 작성해야 한다는 부담이 있다.
    • 그러나 크게 어렵지는 않다.

 

  • 먼저 GitHub project 체크박스를 클릭하고 Project url을 설정한다.

 

💡 빌드 매개변수 추가

  • jar 파일 실행을 하기 위해 빌드 시 매개변수를 추가해야 한다.
    • 여기에서도 꽤나 시간을 잡아먹었다..
  • JenkinsFile에 작성되는 스크립트 내용이 끝나면 실행 내용을 전부 kill 해버리기 때문이다.
    • nohup으로 생성해도 파이프라인 스크립트에 작성했던 내용은 전부 죽여버리기 때문에 함께 kill 된다.
    • 이 설정을 해놓지 않으면 nohup으로 실행시킨 스프링부트 파일이 빌드 배포가 종료됨과 동시에 process kill 된다.
  • 위와 같이 매개변수 명에는 JENKINS_NODE_COOKIE, Value에는 dontKillMe를 추가해 주자.

 

❗️잠깐

  • pipeline 스크립트를 작성하는 경우에는 BUILD_ID 대신에 JENKINS_NODE_COOKIE를 사용해야 한다고 공식문서에 나와있다.

 

✍🏻 JenkinsFile 작성

  • 좀 아래로 내리면 pipeline 작성란이 나온다.
  • 이곳에 스크립트 파일을 작성하면 된다.

 

🥊 scripted Pipeline VS declarative Pipeline

  • Script 입력창 오른쪽 상단에 보면 sample Pipeline을 선택할 수 있다. 
  • 샘플들을 클릭해 보면 뭔가 비슷해 보이면서도 다르게 생겼다.
  • 왼쪽은 Scripted Pipeline이며, 오른쪽은 Declarative Pipeline이다.
    • Scripted Pipeline과 Declarative Pipeline 모두 젠킨스에서 사용되는 파이프라인 스크립트이다.
    • 이 두 가지 파이프라인은 젠킨스를 사용하여 CI/CD(지속적인 통합/지속적인 배포) 워크플로우를 구성하는 데 사용된다.

 

💡 Scripted Pipeline

  • 기존의 젠킨스 파이프라인 스크립트로 Groovy라는 스크립트 언어를 사용하여 만든다.
    • Groovy는 Java 플랫폼에서 실행되는 동적 언어로 자바 문법을 기반으로 여러 기능을 제공한다.
  • Scripted Pipeline은 자유도가 높고 유연하며, 복잡한 워크플로우를 처리하는 데 적합하다.

 

💡 Declarative Pipeline

  • 젠킨스 2.5 버전부터 도입된 새로운 형식의 파이프라인 스크립트이다.
  • 파이프라인의 구조와 단계를 더 명확하게 정의할 수 있도록 설계되었다.
  • 이 역시 Groovy 기반으로 작성되며 Scripted Pipeline에 비해 보다 간결하고 가독성이 높은 문법을 제공한다.
  • 일반적인 CI/CD 워크플로우를 쉽게 작성할 수 있도록 구성 요소를 사전에 정의하고 재사용할 수 있는 템플릿을 제공한다.
  • 파이프라인의 구조를 시각적으로 표현하고 모니터링할 수 있는 기능도 제공한다.

 

📜 정리

  • 이 두 가지 파이프라인의 선택은 프로젝트의 요구 사항과 개발자의 선호도에 따라 달라진다.
  • Scripted Pipeline은 자유로움과 유연성을 원하는 복잡한 워크플로우에 적합하다.
  • Declarative Pipeline은 보다 간결하고 구조화된 워크플로우를 원하는 경우에 유용하다.
  • 관련 내용을 다룬 블로그를 좀 더 찾아본 결과 최근의 CI/CD는 Declarative가 대세라고 하며, Github Actions 역시 YAML 기반의 Declarative를 지원한다고 한다.
    • 나는 보다 간편하고 대세인 Declarative 형식을 사용하기로 결정했다.

 

📌 Jenkinsfile 

  • 이 부분에서도 지난 글과 마찬가지로 작은 부분으로 쪼개 설명한 후 완성본으로 보여주겠다.

 

pipeline {
agent any

stages {...}
}
  • pipeline
    • 파이프라인 블록을 정의한다.
    • 이 블록 안에는 파이프라인의 전체 구조가 포함된다.
  • agent any 
    • 파이프라인이 실행될 에이전트를 지정한다.
    • any는 어떤 에이전트에서라도 파이프라인을 실행할 수 있음을 의미한다.
  • stages
    • 파이프라인의 단계를 정의하는 블록이다.
    • stages 블록 안에는 여러 개의 stage 블록이 포함될 수 있다.
    • 각 stage는 파이프라인의 특정 단계를 나타내며, 단계별로 실행될 작업을 정의할 수 있다.
  • 파이프라인을 실행하면, 정의된 단계와 작업이 순차적으로 실행된다.

 

⌨️ stage('Backup & Clean Workspace')

    stages {
        stage('Backup & Clean Workspace') {
            steps {
                script {
                    sh """
                        rm -rf /Users/wonyong/.jenkins/workspace/backup/*
                        cp -r /Users/wonyong/.jenkins/workspace/jenkinsTest /Users/wonyong/.jenkins/workspace/backup
                        rm -rf *
                    """
                }
            }
        }
  • stages 안에 정의한 각 stage가 순차적으로 실행되는데 그 이름을 직접 설정할 수 있다.
  • sh """ """ 안에 내용을 적으면 개행된 부분도 전부 스크립트 처리가 된다.
  • rm -rf /Users/wonyong/.jenkins/workspace/backup/*
    • 기존에 jenkins workspace 내부에 backup 폴더를 생성하고 내부에 backup 폴더를 rm -rf 명령어를 비운다.
  • cp -r /Users/wonyong/.jenkins/workspace/jenkinsTest /Users/wonyong/.jenkins/workspace/backup
    • /Users/wonyong/.jenkins/workspace/jenkinsTest(깃허브로부터 새로 클론 한 폴더)를 위에서 비워둔 백업폴더에 넣는다.
    • r 옵션은 재귀적으로 복사하라는 의미이며, 디렉터리를 복사할 때 하위 디렉터리와 파일도 함께 복사한다.
  • rm -rf *
    • 기존 워크스페이스의 jenkinsTest 폴더 내부 전체를 비운다.
  • 즉, stage에서 실행되는 기본 폴더의 현재 경로는 해당 워크스페이스 내 빌드 테스트 이름이다.(자동 생성된다.)
  • 이 부분은 굳이 필요없다. 나의 경우 backup 폴더가 필요하다고 판단해 만들었다.

 

📌 stage('Github Clone')

stage('Github Clone') {
            steps {
                git branch: 'main',
                credentialsId: 'github_token', 
                url: 'https://github.com/wonyongg/jenkinsTest'
            }
        }
  • github 레파지토리에서 clone 하는 단계이다.
  • 여기서 credentialsId는 젠킨스 기본 설정에서 만든 Credential 정보이다.
  • 위의 링크와 동일하다. 이곳으로 들어가 credentials 정보를 등록하는 방법을 참고하자.

 

📌 stage('Build')

stage('Build') {
            steps {
                script {
                    sh """
                        cd forJenkins
                        perl -i -pe 'y|\r||d' gradlew
                        ./gradlew build 
                    """
                }
            }
        }
  • 이 부분이 상당히 애먹은 부분이다.
  • 나는 이 테스트를 인텔 맥북 프로와 m2 맥북 에어 두 기기로 진행했다.
  • 인텔 맥북에서는 perl -i -pe 'y|\r||d' gradlew 이 명령어를 넣을 필요가 없다.
  • m2 맥북에서는 이 명령어 없이 작동시켰을 때 에러가 발생했다.
    • 이 문제가 왜 생기는지 정확히 알 수는 없으나 m2 맥북 로컬에서 깃허브로 프로젝트를 푸시하고 이를 젠킨스에서 가져와 로컬에 다시 클론 하는 과정에서 gradlew 파일이 변하는 것 같다. 운영체제의 차이라고 하는데 인텔 맥북에서는 아무 문제 없이 됐지만 m2 맥북에서는 gradlew를 실행하자 "zsh: ./gradlew: bad interpreter: /bin/sh^M: no such file or directory"와 같은 에러가 발생했다.
    • 이는 주로 윈도우 환경에서 생성된 파일을 리눅스/유닉스 환경에서 사용할 때 발생하는 문제라고 한다.
    • 정확한 원인은 모르겠다. 깊게 찾아보지 않았다. 아무튼 같은 에러가 발생한다면 저 명령어를 추가해 주자.
  • perl : Perl 인터프리터를 실행하는 명령어이다.
  • -i : "in-place" 모드로 파일을 수정하는 옵션이다. 이 옵션을 사용하면 원본 파일이 변경된다.
  • -pe : -p 옵션은 입력 파일을 한 줄씩 읽어 들이고, -e 옵션은 주어진 Perl 코드를 실행한다.
  • 'y|\r||d' : y 연산자는 문자열에서 특정 문자를 다른 문자로 치환하는 역할을 한다. 여기서는 \r 문자를 삭제한다. d 플래그는 치환된 문자열을 삭제하라는 의미이다.
  • 주어진 명령어를 실행하면 gradlew 파일에서 \r 문자가 삭제된 파일이 생성된다. (\r 문자가 추가되나보다..)

 

📌 stage('Deploy')

stage('Deploy') {
            steps {
                script {
                    sh """
                        cd forJenkins/script && perl -i -pe 'y|\r||d' deploy.sh switch.sh default_switch.sh
                        ./deploy.sh
                    """
                }
            }
        }
    }
}
  • 이 부분도 마찬가지이다. 위와 같은 에러가 발생한다면 perl -i -pe 'y|\r||d' deploy.sh switch.sh default_switch.sh 이 명령어를 추가해 주면 된다.
  • jenkinsTest의 forJenkins 프로젝트 파일로 들어가 script의 deploy.sh 파일을 실행함으로써 이전 글에서 작성한 배포 스크립트 파일을 실행시키는 것이다.

 

⌨️ JenkinsFile 완성본

pipeline {
    agent any
    
    stages {
        stage('Backup & Clean Workspace') {
            steps {
                script {
                    sh """
                        rm -rf /Users/wonyong/.jenkins/workspace/backup/*
                        cp -r /Users/wonyong/.jenkins/workspace/jenkinsTest /Users/wonyong/.jenkins/workspace/backup
                        rm -rf *
                    """
                }
            }
        }
        
        stage('Github Clone') {
            steps {
                git branch: 'main',
                credentialsId: 'github_token', 
                url: 'https://github.com/wonyongg/jenkinsTest'
            }
        }
        
        stage('Build') {
            steps {
                script {
                    sh """
                        cd forJenkins
                        perl -i -pe 'y|\r||d' gradlew
                        ./gradlew build 
                    """
                }
            }
        }
        
        stage('Deploy') {
            steps {
                script {
                    sh """
                        cd forJenkins/script && perl -i -pe 'y|\r||d' deploy.sh switch.sh default_switch.sh
                        ./deploy.sh
                    """
                }
            }
        }
    }
}
  • perl -i -pe 'y|\r||d' 이 명령어가 필요 없다면 빼서 사용하면 된다.

 

📌 배포 테스트해 보기

  • 드디어 배포 테스트이다.
  • 여기서 정리를 하겠다.
  • 테스트는 github으로 푸시하여 webhook으로 빌드하는 과정을 생략하고 직접 빌드로 진행한다.
  • 직접 빌드
    • 버튼 클릭 
  • stage('Backup & Clean Workspace')
    • 로컬 내 워크스페이스에 기존 백업 파일 삭제 -> 기존 프로젝트 파일 백업 파일로 복사 -> 기존 프로젝트 파일 삭제 
  • stage('Github Clone')
    • jenkins credential 사용 -> github 레파지토리에서 클론 -> 워크스페이스 프로젝트 파일 생성(있다면 덮어쓰기)
  • stage('Build')
    • cd 명령어로 프로젝트 파일로 들어가 ./gradlew build 실행
  • stage('Deploy')
    • 지난 글에 작성한 deploy.sh 실행
      • if) 블루, 그린 모두 실행중이 아니라면 deploy.sh -> default_switch.sh -> 블루 포트 실행
      • else if) 블루가 실행중이 아니라면 deploy.sh -> switch.sh -> 그린 포트 실행 -> 블루 kill
      • else if) 그린이 실행중이 아니라면 deploy.sh -> switch.sh -> 블루 포트 실행 -> 그린 kill
      • 스크립트 종료, 배포 완료
  • Dashboard에서 해당 item에 들어가 "파라미터와 함께 빌드"를 클릭한다.

 

  • 이후 매개변수 값을 확인하고 초록색 빌드 버튼을 누른다.

 

  • 빌드가 진행 중이고 성공할 것이다.

 

📜 파이프라인 로그

Skip to content
[Jenkins]Jenkins
검색 (⌘+K)
1
황원용

로그아웃
Dashboard
jenkinsTest
#21
Status
Changes
Console Output
View as plain text
Edit Build Information
Delete build ‘#21’
Parameters
Git Build Data

Restart from Stage
Replay
Pipeline Steps
Workspaces
Previous Build
콘솔 출력
Started by user 황원용
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /Users/wonyong/.jenkins/workspace/jenkinsTest
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Backup & Clean Workspace)
[Pipeline] script
[Pipeline] {
[Pipeline] sh
+ rm -rf /Users/wonyong/.jenkins/workspace/backup/jenkinsTest
+ cp -r /Users/wonyong/.jenkins/workspace/jenkinsTest /Users/wonyong/.jenkins/workspace/backup
+ rm -rf forJenkins
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Github Clone)
[Pipeline] git
The recommended git tool is: NONE
using credential github_token
 > git rev-parse --resolve-git-dir /Users/wonyong/.jenkins/workspace/jenkinsTest/.git # timeout=10
Fetching changes from the remote Git repository
 > git config remote.origin.url https://github.com/wonyongg/jenkinsTest # timeout=10
Fetching upstream changes from https://github.com/wonyongg/jenkinsTest
 > git --version # timeout=10
 > git --version # 'git version 2.39.2 (Apple Git-143)'
using GIT_ASKPASS to set credentials 
 > git fetch --tags --force --progress -- https://github.com/wonyongg/jenkinsTest +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git rev-parse refs/remotes/origin/main^{commit} # timeout=10
Checking out Revision 1e4436fc668e08130c8645ca6e4cbbff4bddec3f (refs/remotes/origin/main)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 1e4436fc668e08130c8645ca6e4cbbff4bddec3f # timeout=10
 > git branch -a -v --no-abbrev # timeout=10
 > git branch -D main # timeout=10
 > git checkout -b main 1e4436fc668e08130c8645ca6e4cbbff4bddec3f # timeout=10
Commit message: "exit 0"
 > git rev-list --no-walk 1e4436fc668e08130c8645ca6e4cbbff4bddec3f # timeout=10
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Build)
[Pipeline] script
[Pipeline] {
[Pipeline] sh
+ cd forJenkins
+ perl -i -pe 'y|
||d' gradlew
+ ./gradlew build
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details
> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootJarMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :check
> Task :build

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.4/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 5s
7 actionable tasks: 7 executed
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Deploy)
[Pipeline] script
[Pipeline] {
[Pipeline] sh
+ cd forJenkins/script
+ perl -i -pe 'y|
||d' deploy.sh switch.sh default_switch.sh
+ ./deploy.sh
-> 👨🏻‍💼 <Jenkins 배포 시작>
-> 📦 현재 구동중인 Spring Boot Profile 확인
-> ⚠️ profile not found : 일치하는 Profile이 없습니다.
-> ⚠️ set default profile : 기본 Profile인 blue를 할당합니다.
-> ⚠️ 블루, 그린이 모두 동작중이 아니었으므로, blue만 배포하고 스크립트를 종료합니다.
-> 🚚 blue 배포
-> 🚚 nohup java -jar /Users/wonyong/.jenkins/workspace/jenkinsTest/forJenkins/build/libs/forJenkins.jar --spring.profiles.active=blue > /dev/null &
-> 🚑 blue : 10초 후 Health Check Start
-> 🚑 curl -s http://localhost:8081/actuator/health
-> 🚨 [1 번째 Health Check]
-> ✅  Health Check 성공!!!
-> 🔄 NGINX : Default Port로 프록시 설정
-> ⚡️ set default port : 기본 포트인 8081 를 할당합니다.
-> 🔄 NGINX 컨테이너 내의 프록시 방향 설정
-> 🥳 set $service_url http://127.0.0.1:8081;
-> 🔄 NGINX 재시작
-> 👨🏻‍💼 <Jenkins 배포 성공> 스크립트를 종료합니다.
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
REST API
Jenkins 2.433
  • 기본적으로 blue, green 모두 작동 중이 아닐 시 blue 포트로 스프링부트를 실행시키고 프록시 방향을 blue로 설정한다.

 

📌 blue -> green 프록시 변경

-> 👨🏻‍💼 <Jenkins 배포 시작>
-> 📦 현재 구동중인 Spring Boot Profile 확인
-> 💡 현재 구동중인 Profile : blue
-> 🚚 green 배포
-> 🚚 nohup java -jar /Users/wonyong/.jenkins/workspace/jenkinsTest/forJenkins/build/libs/forJenkins.jar --spring.profiles.active=green > /dev/null &
-> 🚑 green : 10초 후 Health Check Start
-> 🚑 curl -s http://localhost:8082/actuator/health
-> 🚨 [1 번째 Health Check]
-> ✅  Health Check 성공!!!
-> 🤼 NGINX 포트 스위칭 스크립트 시작
-> 💡 현재 구동중인 Spring Boot Port : blue
-> ⚡️ 현재 NGINX 프록시 포트 : 8081
-> ⚡️ 변경할 NGINX 프록시 포트 : 8082
-> 🔄 NGINX 컨테이너 내의 프록시 방향 변경
-> 🥳 set $service_url http://127.0.0.1:8082;
-> ✅ NGINX 재시작
-> 🔫 기존 포트인 blue 포트 : 8081 KILL
-> 📦 blue 의 PID : 3962
-> 👨🏻‍💼 <Jenkins 배포 성공> 스크립트를 종료합니다.

  • blue가 살아있을 때 green을 살리고 nginx의 프록시 방향을 변경한 후 blue를 kill 한다.

 

📌 green -> blue 프록시 변경

-> 👨🏻‍💼 <Jenkins 배포 시작>
-> 📦 현재 구동중인 Spring Boot Profile 확인
-> 💡 현재 구동중인 Profile : green
-> 🚚 blue 배포
-> 📦 blue 의 PID : 6668
-> 🚚 nohup java -jar /Users/wonyong/.jenkins/workspace/jenkinsTest/forJenkins/build/libs/forJenkins.jar --spring.profiles.active=blue > /dev/null &
-> 🚑 blue : 10초 후 Health Check Start
-> 🚑 curl -s http://localhost:8081/actuator/health
-> 🚨 [1 번째 Health Check]
-> ✅  Health Check 성공!!!
-> 🤼 NGINX 포트 스위칭 스크립트 시작
-> 💡 현재 구동중인 Spring Boot Port : green
-> ⚡️ 현재 NGINX 프록시 포트 : 8082
-> ⚡️ 변경할 NGINX 프록시 포트 : 8081
-> 🔄 NGINX 컨테이너 내의 프록시 방향 변경
-> 🥳 set $service_url http://127.0.0.1:8081;
-> ✅ NGINX 재시작
-> 🔫 기존 포트인 green 포트 : 8082 KILL
-> 📦 green 의 PID : 6596
-> 👨🏻‍💼 <Jenkins 배포 성공> 스크립트를 종료합니다.

  • green가 살아있을 때 blue을 살리고 nginx의 프록시 방향을 변경한 후 green를 kill 한다.

 

📜 최종정리

  • ngrok으로 github webhook 연결하여 테스트하는 부분은 생략했다.
    • 같은 설명 반복이기도 하고 결국 외부 서버로 연결하는게 목표이기 때문에 굳이 로컬로 ngrok을 사용하여 외부에 노출하는 과정까지 보여줄 필요가 없다고 판단했다.
    • ngrok으로 외부에 서버를 오픈하고 웹훅으로 배포 자동화 + 무중단 배포 테스트를 하고 싶다면 여기를 참고하여 진행하면 된다.
  • 실제 외부 서버로 jar 파일을 보내기 위해서는 ssh 연결로 진행할 텐데 그 부분은 실무에 적용해 보면서 경험해 볼 예정이다.
    • 오히려 그 부분이 쉬울 수 있다. 로컬에서 로컬로 보내는 게 생각보다 까다로운 부분이 많았다.
    • 기본적으로 로컬 PC는 macOS 기반이라 리눅스와는 약간의 차이가 있는 부분(sudo 명령어도 비밀번호를 입력해야 하기 때문에 사용할 수 없다든가) 등 사소한 문제를 모두 해결하는데 꽤나 시간이 걸렸다.

 

 

 

jenkins 관련 구현 시 참고했거나 에러 핸들링에 도움을 준 링크이다.

구현

https://hyunminh.github.io/nonstop-deploy/

https://hudi.blog/zero-downtime-deployment-with-jenkins-and-nginx/

https://hudi.blog/continuous-deploy-with-jenkins-1-backend/

https://stir.tistory.com/252

https://iizz.tistory.com/341

https://wiki.jenkins.io/display/JENKINS/ProcessTreeKiller

 

 

에러 핸들링

https://be-developer.tistory.com/14

https://narup.tistory.com/224

https://stackoverflow.com/questions/8646762/cannot-run-program-gradle-in-jenkins

https://stackoverflow.com/questions/17131249/how-to-solve-bad-interpreter-no-such-file-or-directory

 

 

728x90