DevOps/CI&CD

Jenkins를 활용한 CI/CD 구축(4/4) - Pipeline 구축

Code Maestro 2023. 4. 27. 13:57
728x90

※ 1, 2, 3편을 안 보고 이 포스팅을 볼 경우에 이해가 안 될 수가 있습니다. 1, 2, 3편을 연달아 보시는 걸 추천드립니다. 

1. 파이프 라인이란?

출처:https://www.lambdatest.com/blog/jenkins-pipeline-tutorial/

단일 작업이 아니라 연쇄적인 작업들을 이어주게 합니다.  Jenkins에는 파이프 라인을 구축하는 2가지 방식이 있습니다. 

 

 

1) Delivery Pipeline

Item 타입의 프로트타입의 연장선입니다. Jenkins 프로젝트 마지막의 빌드 후 조치에서 Build other projects를 선택. Projects to build에서 첫 번째 단계 이후에 실행할 Jenkins 프로젝트명을 적으면 됩니다. (반드시 두 번째 단계에서 실행할 Jenkins 프로젝트가 구성되야 한다.)

 

Trigger only if build is stable을 선택하여 안정적으로 첫 번째 단계가 끝났을 경우에 다음 jenkins 프로젝트가 실행됩니다. 이렇게 연쇄적으로 실행이 되게끔 파이프 라인을 구축할 수 있습니다. 

 

 

2) Jenkins Pipeline

Jenkins에서 프로젝트를 생성할 때, 위의 사진처럼 Pipeline을 선택하면 생성할 수 있습니다. Item 대신 script를 활용하여 좀 더 다이나믹하게 자신이 원하는 형태로 구성할 수 있습니다. 

 

크게 두 가지 방식이 있는데 하나는 Declarative, 다른 하나는 Scripted(Groovy+DSL) 방식입니다.

 

이 두 방식의 차이점은 Declarative의 경우 실패할 때 그 다음 경우로 진행하지 않는 반면, Scripted는 그 다음 경우로 계속 진행합니다. 이외에도 시작 시 유효성 검사 유무, 제어문, option의 차이점이 있습니다. 

 

이 포스팅에서는 Declarative 문법을 활용하여 파이프 라인을 구축하겠습니다.  

 

2. Declarative 문법

Declarative 문법은 위와 같습니다.  

 

문법을 기반으로 이렇게 단계별로 파이프 라인을 나눌 수 있습니다. 

 

Declarative 문법을 사용하고 싶은 경우에는 

Pipeline Syntax를 클릭하여 

 

Item에서처럼 설정값을 넣어주고, Generate Pipeline Script를 클릭하면

파이프 라인 문법에 맞게끔 생성된 걸 확인할 수 있습니다. 이걸 복사하고 붙여넣기하면 됩니다. 

 

 

이제 이를 기반으로 파이프 라인을 구축해보겠습니다. 

 

3. Pipeline Flow 

 제가 구축할 파이프 라인은 아래와 같습니다.

 

  1. Git Clone
  2. SonarQube로 코드 분석
  3. Spring Boot 파일을 Gradle을 이용하여 Jar 파일로 빌드하기
  4. Jar 파일을 Docker 파일을 이용하여 이미지 빌드하기
  5. 생성된 도커 이미지를 docker hub에 push하기
  6. 기존 도커 이미지 삭제
  7. publish over ssh를 사용하여 배포 서버에 3편에 제작한 쉘 스크립트 실행하여 배포하기

요약을 하면  

 

( Git Clone => SonarQube => Compile => Image Build => Image Push => Image Clean => Deployment )

 

단계로 진행할 겁니다. 

 

3-1. Git Clone

Build Triggers에는 poll scm을 체크하여 2편처럼 Github에 master 브랜치에서 Push가 일어나면, 알아서 코드를 Pull하게끔 설정했습니다. 

파이프 라인 스크립트 문법은 위와 같습니다.

 

cp 명령어를 왜 한 것인지 모르겠다는 분들은 이 글을 참조하시면 됩니다. 

3-2. SonarQube 

Jenkins의 SonarQube 설정은 이 글을 참조하시길 바랍니다.

 

이 포스팅에서는 SonarQube의 Pipeline Script를 어떻게 작성하는지만 나타내겠습니다. 

 

 

 

이전의 SonarQube 글처럼 설정했다면, 위의 이미지처럼 시스템 설정에서 SonarQube가 설정된 걸 확인할 수 있습니다. 

 

 

 

다시 파이프라인으로 돌아가서

withSonarQubeEnv에는 시스템 설정에서 등록한 sonarqube의 Name 등록해줍니다. 여기서 './gradlew sonarqube' 명령어로 gradle에서 sonarqube 검사를 실시하게끔 만듭니다.

 

 

파이프 라인을 실행하고, Sonarqube 설치된 ec2의 URL을 확인해보면 

 

 

검사가 확인할 있습니다. 

 

3-3. Compile

간단히 gradle로 build하면 끝입니다. 

 

3-4. Image Build

Jenkins가 설치된 EC2에 반드시 Docker가 설치되야 합니다. Docker 설치는 https://dongle94.github.io/docker/docker-ubuntu-install/ 을 참고하시길 바랍니다. 

 

Docker Image를 만들기 위해서는 Dockerfile이 필요합니다. Dockerfile의 내용은 3편을 참고하시면 됩니다. 

 

저는 /home/ubuntu/spring 위치에 Dockerfile을 위치시켰습니다. 

/home/ubuntu/spring에 위치한 Dockerfile을 파이프 라인 worksapce의 jar 파일이 있는 위치로 copy 시켰습니다.

이후 Docker 파일을 이용하여 image를 생성했습니다.  

 

3-5. Image Push

DockerHub token 등록해 줘야 jenkins 통해 docker push  Docker Hub 로그인 과정에서 권한 관련 오류가 나지 않습니다. 이를 위해 Docker Hub token을 발급받습니다. 

 

Docker Hub에 접속해서 Account Settings 클릭.

 

Security > New Access Tokens 클릭. Access Token Description 입력.(원하는 이름으로 입력하면 된다.) 그 다음 Generate 클릭해 줍니다. 

 

Token 복사.  번밖에 노출이  되므로  어딘가에 저장을 해야 합니다.

 

 

이제 Jenkins로 돌아와서 자신의 도커 허브 토큰을 등록합시다. 

 

Jenkins 관리

 Manage Credentials

 

Add Credentials

 

Username: Docker hub id 입력, Password: Docker hub token 입력, ID: 원하는 credetial 이름 지정

 

위의 형식대로 입력하고 Create를 누르면 계정 설정은 끝났습니다.

 

이제 파이프 라인 구축을 해보겠습니다.

 

환경 변수에 repository는 docker hub 아이디/repository명 형식으로 지정하고, DOCKERHUB_CREDENTIALS은 jenkins에 위의 과정에서 등록한 docker hub credentials 이름을 넣어줍니다. 

 

이전에 등록한 docker hub token 기반으로 도커 허브에 접속하여 Docker hub에 이미지 push 해주면 끝입니다. 

 

3-6. Image Clean

 

Jenkins 서버에는 이제 이미지가 필요없기에 기존 도커 이미지를 삭제해줍니다.

 

 

3-7. Deployment 

 

3편에 만든 쉘 스크립트를 실행만 시켜도 돼서 publish over ssh 대신 ssh를 사용하면 되지만, 저는 jar 파일도 같이 옮기고 싶기에 publish over ssh 방식을 선택했습니다. 

 

        stage('Ssh Publisher - Deployment'){
            steps{
                sshPublisher(publishers: [sshPublisherDesc(configName: 'EC2-hospital-portfolio', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''sudo docker rmi -f mac20010/hospital
~/backend/deploy.sh
docker rmi -f $(docker images -f "dangling=true" -q)
''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/backend/', remoteDirectorySDF: false, removePrefix: 'build/libs/', sourceFiles: 'build/libs/hospital-0.0.1-SNAPSHOT.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
            }

 

배포 파이프라인 스크립트는 위와 같습니다. 

 

배포 스크립트(deploy.sh)를 실행하기 전에 sudo docker rmi -f mac20010/hospital 명령어를 한 이유는 이전에 docker hub에서 pull한 이미지를 갱신시키기 위함입니다. 

 

쉽게 설명하면 배포 스크립트에서 docker-compose를 실행하게 되는데, 이때 사용하는 이미지는 mac20010/hospital라는 기존 ec2에 도커 허브에 pull한 이미지를 사용합니다. 

 

저는 이미지를 계속 갱신시키고 싶은데 기존의 이미지가 계속 남아있어서 도커 허브에 pull을 못하는 겁니다. 이에 sudo docker rmi -f mac20010/hospital 명령어로 기존 이미지를 삭제.

 

deploy.sh를 실행하면, docker-compose이 실행되어 mac20010/hospital라는 이미지 이름이 배포 ec2 서버에 없기에 도커 허브에서 pull을 하게 됩니다. 즉, docker hub에 갱신된 이미지를 계속 pull하기 위해 이미지를 삭제했다고 생각하시면 됩니다. 

 

deploy.sh 쉘 스크립트가 끝났다면, docker rmi -f $(docker images -f "dangling=true" -q 명령어로 이름이 없는 이미지를 삭제하게 합니다. 

 

이러면 배포 ec2 서버에 갱신된 이미지만 남게됩니다.

 

 

 

흐름을 간단히 설명하면 아래와 같습니다. 

 

기존의 도커 이미지를 삭제 (docker rmi -f mac20010/hospital 명령어)

 

docker compose에서 새롭게 빌드한 docker image pull한다. (deploy.sh 실행)

 

스크립트가 끝나면 이전의 이미지가 삭제된다. (docker rmi -f $(docker images -f "dangling=true" -q 명령어)

 

4. 마무리

전체적인 파이프 라인이 구축이 완성되었습니다. 

 

전체적인 Pipeline 스크립트 내용은 아래와 같습니다. 

pipeline{
    environment {
        repository = "mac20010/hospital" 
        DOCKERHUB_CREDENTIALS = credentials('dockerhub-login')
    }
    
    triggers {
        pollSCM('* * * * *')
    }
        
    agent any
    stages{
        
        stage('Git Clone'){
            steps{
                git 'https://github.com/kimjungwon2/hospital'
                sh '''sudo cp /home/ubuntu/spring/application-jwt.yml /var/lib/jenkins/workspace/Backend-Pipeline/src/main/resources/application-jwt.yml
sudo cp /home/ubuntu/spring/application-db.yml /var/lib/jenkins/workspace/Backend-Pipeline/src/main/resources/application-db.yml
sudo cp /home/ubuntu/spring/application-aws.yml /var/lib/jenkins/workspace/Backend-Pipeline/src/main/resources/application-aws.yml'''
            }
        }
        
        stage('SonarQube'){
            steps{
                withSonarQubeEnv('SonarQube_Server') {
                    sh '''chmod +x gradlew
                    ./gradlew sonarqube'''
                }
            }
        }
        
        stage('Compile'){
            steps{
                sh './gradlew clean build'
            }
        }
        
        stage('Build Image') { 
          steps { 
              script { 
                  sh '''sudo cp /home/ubuntu/spring/Dockerfile /var/lib/jenkins/workspace/Backend-Pipeline/build/libs/
                  cd /var/lib/jenkins/workspace/Backend-Pipeline/build/libs
                  docker build -t $repository:latest .
                  '''
              }
          }
        }
        
        stage('Image Push') { 
          steps { 
              script { 
                  sh '''echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin
                  docker push $repository:latest
                  ''' 
              }
          }
        }
        
        stage('Image Clean') { 
		  steps { 
              sh "docker rmi $repository:latest" 
          }
        } 
        
        stage('Ssh Publisher - Deployment'){
            steps{
                sshPublisher(publishers: [sshPublisherDesc(configName: 'EC2-hospital-portfolio', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''sudo docker rmi -f mac20010/hospital
~/backend/deploy.sh
docker rmi -f $(docker images -f "dangling=true" -q)
''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/backend/', remoteDirectorySDF: false, removePrefix: 'build/libs/', sourceFiles: 'build/libs/hospital-0.0.1-SNAPSHOT.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
            }
        }
        
    }
}

파이프 라인을 실행시켜보면 

 

 

모든 단계가 정상적으로 작동하는 걸 확인할 수 있습니다. 

 

 

긴 과정 따라오시느라 고생 많으셨습니다.

 

블로그 글처럼 완전 똑같이 하는 게 아니라. 전체적인 흐름을 이해하여 자신의 환경에 맞게끔 수정하신다면 각자의 환경에 맞는 CI/CD를 구축하실 수 있을 겁니다. 감사합니다. 

 

728x90