Jenkins의 Mulitbranch Pipeline을 통한 CI/CD

공병주(Chris)·2022년 8월 6일
0
post-thumbnail

Jenkins의 Mulitbranch Pipeline을 통한 CI/CD

우아한테크코스 Level3 팀 프로젝트의 스프린트 2에서 개발 환경에 CI/CD를 적용했습니다.

스프린트 3에서는 팀원들과 1.0을 배포하기로 결정했고 운영 환경에도 CI/CD 환경을 구축하기로 했습니다.

기존의 Pipeline

기존에는 dev 환경에서만 CI/CD가 이루어져있었습니다. 따라서, Jenkins의 item중 pipeline을 사용했었습니다. 아래가 Github Repository에 push가 일어났을 때 실행되었던 script입니다.

node {
    stage('Ready'){
        sh "echo 'Ready'"
        git branch: 'dev',
            credentialsId: 'sokdak_hook',
            url: 'https://github.com/woowacourse-teams/2022-sokdak'
    }
    
    stage('Build'){
        sh "echo 'Build Jar'"
        dir('backend/sokdak') {
            sh './gradlew bootJar'
        }
    }

		stage('Test'){
	      sh "echo 'Test'"
	      dir('backend/sokdak') {
	        sh './gradlew clean test'
	      }
    }
    
    stage('Deploy'){
        withCredentials([sshUserPrivateKey(credentialsId: "sokdak-pem", keyFileVariable: 'my_private_key_file')]) {
            def remote = [:]
            remote.name = "sokdak-pem"
            remote.host = "{was_server_private_ip}"
            remote.user = "ubuntu"
            remote.allowAnyHosts = true
            remote.identityFile = my_private_key_file
            
            sh "echo 'Deploy AWS'"
            dir('backend/sokdak/build/libs'){
                sh 'scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@{was_server_private_ip}:/home/ubuntu/sokdak'
            }
            stage("Post Deploy"){
                sh 'ssh -i ${my_private_key_file} ubuntu@{was_server_private_ip} "cd sokdak && ./deploy.sh"'
            }
        }
    }
}

해당 Pipeline에서 dev와 main 브랜치를 구분. 즉, dev에서 push가 일어나는 것과 main에서 push가 일어나는 것을 구분하기 위해서는 Pipeline Script에서 webhook의 branch 값을 통해 분기 해주어야 했습니다.

Multibranch Pipeline

따라서, Jenkins에서 Branch에 따른 Pipeline을 지원하는 Multi Branch Pipeline을 사용하기로 결정했습니다.

The Multibranch Pipeline project type enables you to implement different Jenkinsfiles for different branches of the same project. In a Multibranch Pipeline project, Jenkins automatically discovers, manages and executes Pipelines for branches which contain a Jenkinsfile in source control.

Multibranch Pipeline은 같은 프로젝트에서 브랜치마다 다른 Jenkinsfile을 구현하도록 해줍니다. Multibrach Pipeline 프로젝트에서, 젠킨스는 자동으로 Jenkinsfile을 가지고 있는 Branch의 Pipeline을 찾고, 발견하고 실행해줍니다.

출처 : https://www.jenkins.io/doc/book/pipeline/multibranch/

Git에서 특정 이벤트(ex. push, merge 등)가 발생했을 때, 해당 branch에 존재하는 Jenkinsfile을 실행해주는 것입니다.

Jenkinsfile

그렇다면 Jenkinsfile은 무엇일까요?

Jenkinsfile is a text file that contains the definition of a Jenkins Pipeline and is checked into source control

Jenkinsfile은 Jenkins Pipeline에 대한 정의를 담고 있고, 소스 컨트롤에 사용되는 텍스트 파일입니다.

출처 : https://www.jenkins.io/doc/book/pipeline/jenkinsfile/

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                echo 'Building..'
								# do something for Build
            }
        }
        stage('Test') {
            steps {
                echo 'Testing..'
								# do something for Test
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying....'
								# do something for Deployment
            }
        }
    }
}

위와 같이 Pipeline에서 어떤 일을 수행할 지 정의하는 것입니다.

Multibranch Pipeline 세팅 방법

그러면 Multibranch Pipeline을 생성해보겠습니다.

0. Multibranch Scan Webhook Trigger 플러그인 설치

DashBoard > Jenkins 관리 > 플러그인 관리에서 Multibranch Scan Webhook Trigger를 설치합니다.

All multibranch projects comes with build in periodically scan trigger that polls scm and check which branches has changed and than build those branches
This is a Jenkins plugin that add functionality to do this scan on webhook:

모든 Multibranch 프로젝트(Multibranch Pipeline)에는 scm을 폴링하고, 어떤 브랜치가 변경되었는지 확인하고, 해당 브랜치를 빌드하는 역할을 주기적으로 해주는 Scan Trigger가 내장되어있습니다.
이것(Multibranch Scan Webhook Trigger)은 웹훅에 대해 해당 스캔을 진행하도록 하는 기능을 추가하는 플러그인입니다.

출처 : https://plugins.jenkins.io/multibranch-scan-webhook-trigger/

간단하게, webhook이 날아왔을 때, 어떤 브랜치가 변경되었는지 확인하고, 해당 브랜치의 pipeline을 실행하도록하는 플러그인입니다.

1. Multibranch Pipeline 생성

2. Branch Sources 설정

2-1. Github 연결

먼저 Credentials를 등록해주어야 합니다. Add를 누르고, 아래의 창에 값들을 입력합니다.

  • Kind : Username with password
  • Username : 본인의 Github 아이디
  • Password : Github의 Develop settings > Personal access tokens에서 토큰을 발급받아서 입력
  • ID : Jenkins에서 해당 Credentials를 식별할 값

그 후, URL에 CI/CD 환경을 구축할 Repository의 주소를 적고 Validate 버튼을 통해 등록한 Credentials를 사용할 수 있는지 확인합니다.

2-2. Behaviours 설정

다음과 같이 Behaviours를 설정해줍니다.

Filter by name(with regular expression)Filter by name(with wildcards)는 Add 버튼을 눌러서 추가해줘야 합니다.

Filter by name은 Discover 방법으로, 어떤 CI/CD 환경을 구축할 Branch 이름을 정규식과 wildcard 방식으로 적어주면 됩니다.

3. Build Configuration

Pipeline을 빌드할 방법을 설정해줍니다.

저희는 Jenkinsfile을 통해 Pipeline을 구축할 것이기 때문에, Mode를 by Jenkinsfile로 설정합니다.

Script path는 Jenkinsfile의 경로입니다. default인 Jenkinsfile을 path로 지정한다면
아래와 같이 Repository의 최상단으로 경로가 지정이 됩니다.

❗️ Jenkinsfile은 자동으로 생성되지 않고, 직접 만들어야 합니다.

4. Webhook 연결

다음으로, Repository에 Push가 일어났을 때, Jenkins에게 push가 일어났음을 알려줄 Webhook을 연결해보겠습니다.

4-1. Webhook 생성

Repository Setting에서 Webhook을 하나 생성합니다. 저희는 push가 일어났을 때만, webhook을 보내도록 했습니다.

다음으로, Multibranch Pipeline의 경우에는 uri를 아래와 같이 설정해주어야합니다.

  • {jenkins EC2 public ip}/multibranch-webhook-trigger/invoke?token={토큰값}

4-2. Multibranch Scan Webhook Trigger 설정

그 후, Multibranch Scan Webhook Trigger 플러그인에 대한 설정을 해보겠습니다.

위에서 Webhook을 생성할 때 적었던 token 값을 Trigger Token 값에 적어줍니다.

이제 Github Repository와 Jenkins의 연결 설정이 완료되었습니다.

위의 설정들을 하고, Save를 누르면 Multibranch Pipeline이 만들어집니다.
Multibranch Pipeline은 생성되거나 Pipeline의 설정이 바뀔 때, Jenkins가 아래와 같이 Repository의 branch들을 check합니다.

주의 깊게 볼 점

Multibranch Pipeline을 구성할 때, dev와 main을 브랜치들에 merge 혹은 push가 되었을 때만 CI/CD Pipeline이 실행되도록 Filter By Name을 설정했습니다.

따라서, Jenkins가 main과 dev 브랜치에 대해서만 Jenkinsfile이 있는지 확인합니다.

5. Jenkinsfile 작성

Setting이 끝났으니 실제로 branch에 push가 일어났을 때, 실행할 파이프라인을 작성해서 위의 Build Configuration에서 설정했던, Script path에 지정한 위치에 Jenkinsfile을 위치시킵니다.

pipeline{
  agent any
  
  tools {
      gradle 'gradle'
  }
  
  stages{
    stage('Ready'){
      steps{
        sh "echo 'Ready'"
        git branch: 'dev',
          credentialsId: 'sokdak_hook',
          url: 'https://github.com/woowacourse-teams/2022-sokdak'
      }
    }

    stage('Test'){ # Test 실행
      steps{
        sh "echo 'Test'"
        dir('backend/sokdak') {
          sh './gradlew clean test'
        }
      }
    }

    stage('Build'){ # .jar 파일 생성
      steps{
        sh "echo 'Build Jar'"
        dir('backend/sokdak') {
          sh './gradlew bootJar'
        }
      }
    }
    
    stage('Deploy'){
      steps{
        script{
          withCredentials([sshUserPrivateKey(credentialsId: "pem-key", keyFileVariable: 'my_private_key_file')]) {
            def remote = [:]
            remote.name = "pem-key"
            remote.host = "${env.DEV_BACK_IP}"
            remote.user = "ubuntu"
            remote.allowAnyHosts = true
            remote.identityFile = my_private_key_file

            sh "echo 'Deploy AWS'"
            dir('backend/sokdak/build/libs'){ #.jar 파일 EC2로 전송
                sh "scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@${env.DEV_BACK_IP}:/home/ubuntu/sokdak"
            }
						# EC2에 접속하여 배포스크립트 실행
            sh "ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${env.DEV_BACK_IP} 'cd sokdak && ./deploy.sh'"
            sh "echo 'Spring Boot Running'"
          } 
        }
      }
    }
  }
}

EC2 ip를 환경변수 설정

위의 Deploy stage를 보시면, ${env.DEV_BACK_IP}라는 환경변수가 있습니다. Jenkins와 EC2가 한 vpc에 있기 때문에 Jenkins가 EC2의 private ip를 통해 EC2에 접근하지만 ip는 최대한 노출시키지 않는 것이 좋다고 생각하여 환경변수를 설정했습니다.

Jenkins 관리 > 시스템 설정 > Global properties에서 아래와 같이 환경변수를 설정해줄 수 있습니다.

6. ssh, scp 명령을 위한 .pem 파일 credentials로 등록

위의 pipeline을 보시면 .jar파일을 scp 명령어를 통해서 EC2에 전송시키고, ssh를 통해 EC2에 접속해서 배포스크립트를 실행하는데요. EC2 접근을 위한 .pem 파일을 Jenkins의 credentials로 등록해야 합니다.

아래와 같이 Jenkins 관리 > Manage Credentials에서 add credential을 하고 Kind를 Secret text로 해서 등록합니다.

ID는 위의 pipeline에서 작성한 credentialID와 동일해야 합니다.

Secret에는 .pem 파일을 vim으로 열어서 ----BEGIN RSA PRIVATE KEY---—와 ----END RSA PRIVATE KEY---—까지를 모두 복사해서 Secret에 붙혀넣으면 됩니다.

동작 확인

위의 PR을 dev에 merge 시키면, 아래와 같이 CI/CD pipeline이 실행됩니다.

똑같이 main branch에도 Jenkinsfile을 작성하면 main branch에 대한 CI/CD pipeline을 실행시킬 수 있습니다.

저희 팀은 Front-End와 Back-End가 하나의 Repo를 사용하고 있기 때문에, 아래와 같이 Front의 CI/CD의 Script도 작성해주었습니다.

pipeline{
  agent any

  tools {
      gradle 'gradle'
  }

  stages{
    stage('Ready'){
      steps{
        sh "echo 'Ready'"
        git branch: 'dev',
          credentialsId: 'sokdak_hook',
          url: 'https://github.com/woowacourse-teams/2022-sokdak'
        sh "echo '########### CLONE MAIN ###########'"
      }
    }

    stage('FE-Install'){
      steps{
        sh "echo '########### FE MODULE INSTALL START ###########'"
        dir('frontend'){
          sh 'npm i'
        }
        sh "echo '########### FE MODULE INSTALL SUCCESS ###########'"
      }
    }

    stage('FE-Test'){
      steps{
        sh "echo '########### FE TEST START ###########'"
        dir('frontend'){
          sh 'npm run test'
        }
        sh "echo '########### FE TEST SUCCESS ###########'"
      }
    }

    stage('BE-Test'){
      steps{
        sh "echo '########### BE TEST START ###########'"
        dir('backend/sokdak') {
          sh './gradlew clean test'
        }
        sh "echo '########### BE TEST SUCCESS ###########'"
      }
    }

    stage('FE-Build'){
      steps{
        sh "echo '########### FE BUILD START ###########'"
        dir('frontend'){
          sh 'npm run build-prod'
        }
        sh "echo '########### FE BUILD SUCCESS ###########'"
      }
    }

    stage('BE-Build'){
      steps{
        sh "echo '########### BE BUILD START ###########'"
        dir('backend/sokdak') {
          sh './gradlew bootJar'
        }
        sh "echo '########### BE BUILD SUCCESS ###########'"
      }
    }

    stage('FE-Deploy'){
      steps{
        script{
          withCredentials([sshUserPrivateKey(credentialsId: "front-key", keyFileVariable: 'front_key_file')]) {
            sh "echo '########### FE DEPLOY START ###########'"
            dir('frontend'){
                sh "scp -o StrictHostKeyChecking=no -i ${front_key_file} -r dist ubuntu@${env.DEV_FRONT_IP}:/home/ubuntu"
            }
            sh "ssh -o StrictHostKeyChecking=no -i ${front_key_file} ubuntu@${env.DEV_FRONT_IP} 'sudo service nginx restart'"
            sh "echo '########### FE DEPLOY SUCCESS ###########'"
            sh "echo '########### FE COMPLETE ###########'"
          }
        }
      }
    }

    stage('BE-Deploy'){
      steps{
        script{
          withCredentials([sshUserPrivateKey(credentialsId: "back-key", keyFileVariable: 'my_private_key_file')]) {
            sh "echo '########### BE DEPLOY START ###########'"
            dir('backend/sokdak/build/libs'){
                sh "scp -o StrictHostKeyChecking=no -i ${my_private_key_file} *.jar ubuntu@${env.DEV_BACK_IP}:/home/ubuntu/sokdak"
            }
            sh "ssh -o StrictHostKeyChecking=no -i ${my_private_key_file} ubuntu@${env.DEV_BACK_IP} 'cd sokdak && ./deploy.sh'"
            sh "echo '########### BE DEPLOY SUCCESS ###########'"
            sh "echo '########### BE COMPLETE ###########'"
          }
        }
      }
    }
  }

Multibranch Pipeline의 문제점

1. merge가 되었을 때, Compare Branch에 대한 분기가 어렵다

저희 팀은 main에서 dev로 분기가 일어나고, dev에서 be와 fe로 분기가 일어나는 Branch 전략을 가져갔습니다. 따라서, be에서 dev로 merge가 되었을 때와 fe에서 dev로 merge가 되었을 때를 분기하여 script를 실행하도록 하고 싶었습니다. multibranch pipeline가 아닌 pipeline를 사용하면 Generic Webhook Trigger라는 플러그인을 통해 pr에 달린 label을 통해 분기를 처리할 수 있습니다. 하지만 multibranch pipeline은 Generic Webhook Trigger 플러그인을 지원해주지 않습니다.

따라서, Webhook의 payload 값을 직접 추출하여 분기 처리를 해야할 것으로 예상되는데요. 이에 대한 레퍼런스가 부족합니다..

2. Jenkinsfile 관리의 어려움

위에서 Jenkinsfile의 경로를 적을 때, branch 마다 Jenkinsfile의 경로를 적은 것이 아닌, 경로를 하나만 적어주었습니다. 이것이 Jenkinsfile을 git으로 관리하는 것이 어렵게 합니다.

multibranch에서 main 브랜치에서 push가 일어나던 dev 브랜치에서 push가 일어나던 위의 경로에 있는 script를 실행합니다.

문제는 build를 한 file을 전송하고 실행하는 IP주소는 main과 dev가 다르다는 것입니다. 하지만 Jenkinsfile은 하나입니다. 따라서, dev와 main의 Jenkinsfile 경로는 하나로 가져가되, Jenkinsfile에 작성된 IP 주소는 다르게 가져가야 합니다.

때문에, dev에서 main으로 merge를 시킬 때 아래의 작업을 매번 실행해주어야합니다.

  • 운영(main) 환경에 해당하는 IP 주소로 변경시키고 main으로 merge시키고 다시 dev 브랜치에서 Jenkinsfile에 있는 IP 주소를 dev 환경에서의 IP로 수정한다.

임시 해결 방안

위처럼 Multibranch Pipeline을 2개(main용, dev용) 생성하여, 아래와 같이 Filter By Name과 Jenkinsfile의 path를 다르게 지정해주었습니다.

sokdak-main의 configuration

sokdak-dev의 configuration

스프린트 3까지 CI/CD를 통한 배포를 완료해야 했기 때문에, 임시적인 방법을 사용했지만 다른 좋은 방법이 있을 것으로 예상합니다. multibranch pipeline에 시간을 많이 쏟았지만, 과감하게 pipeline으로 전환하는 것도 좋은 방법인 것 같습니다.

끗!

2개의 댓글

comment-user-thumbnail
2022년 8월 6일

대박... 크리스 최고..!!

1개의 답글