Jenkins란 뭘까? CI/CD랑 무슨 관계가 있을까?

SUI·2024년 6월 2일

Java

목록 보기
4/4
post-thumbnail

1️⃣ 젠킨스(Jenkins)란?

젠킨스는 지속적 통합(Continuous Integration, CI)와 지속적 전달(Continuous Delivery, CD)를 지원하는 오픈 소스 자동화 툴이다. 소프트웨어 개발 과정을 자동화하고, 코드 변경을 지속적으로 통합하여 빌드, 테스트, 배포하는 과정을 자동화하여 관리할 수 있도록해준다.


2️⃣ CI/CD란?

사진에서 파란색 부분이 CI, 노란색 부분이 CD에 해당한다.

지속적 통합과 지속적 전달, CI/CD는 소프트웨어 개발 및 배포 프로세스를 지속적으로 통합하고 제공하는 개발 방법론을 말한다.


📑 2-1. CI(Continuous Integration)

CI는 여러 개발자들이 동시에 작업하고 있는 경우, 각자의 변경사항을 중앙 저장소에 지속적으로 통합하는 프로세스를 의미한다. CI는 팀 내의 여러 명의 개발자가 작성한 코드를 자주 통합하여 품질을 지속적으로 검증하기 위해 적용된다. 아래는 CI의 과정은 간단하게 정리한 것이다.

  1. 소스 코드 관리(SCM)
    소스 코드는 중앙 저장소에 저장되며, 이는 모든 변경사항이 추적되고 버전이 관리된다. 이로 인해 개발자는 언제든지 이전 버전의 코드로 롤백할 수 있으며, 다른 개발자가 진행한 작업을 쉽게 확인하고 통합할 수 있다. 이 과정에서 깃(Git) 같은 버전 관리 시스템을 사용하게 된다.
  2. 빌드 자동화
    코드가 저장소에 푸쉬되면, 소프트웨어 빌드가 자동으로 실행된다. 빌드 과정(컴파일, 패키징, 테스트 등)이 자동화되며, 이를 통해 개발자는 코드를 빠르게 통합하고 테스트할 수 있다.
  3. 자동 테스트
    빌드 후, 자동화된 테스트가 실행된다. 이는 변경사항이 기존 시스템에 문제를 일으키지 않는지 확인한다. 이 과정에서 단위 테스트(unit test), 통합 테스트(integration test), 기능 테스트(functional test) 등 다양한 테스트가 수행될 수 있다. 테스트는 코드의 품질을 보장하고, 버그를 식별하고 수정하는 데 도움이 된다.
  4. 결과 피드백
    테스트 결과는 개발 팀에게 피드백으로 제공된다. 이를 통해 팀은 실패한 빌드 또는 테스트를 신속하게 수정할 수 있다.

✒️ 2-1-1. CI를 필요로하는 환경

  • 다수의 개발자가 협업하는 환경
    많은 개발자가 함께 작업할 때, 각자의 코드 변경사항을 효과적으로 통합하지 않으면 코드 충돌이나 버그가 발생할 수 있다. CI는 자주 이루어지는 코드 통합을 통해 이러한 문제를 미리 방지한다.
  • 빠른 반복 개발이 이루어지는 환경
    CI는 빠른 피드백 루프를 제공하여 개발자가 코드 변경사항에 대한 피드백을 신속하게 받을 수 있게 해줍니다. 이는 빠른 개발 주기를 가진 프로젝트에 특히 유용합니다.
  • 품질 관리가 중요한 환경
    CI는 자동 테스트를 통해 코드 품질을 지속적으로 관리하고 향상시킵니다. 이는 특히 소프트웨어 품질이 중요한 분야(예: 의료, 금융 등)에서 중요합니다.
  • 복잡한 프로젝트 환경
    프로젝트의 규모가 크거나 복잡성이 높아질수록, 코드 변경 사항의 영향을 예측하고 관리하는 것이 어려워집니다. CI는 이러한 복잡성을 관리하고 통제하는 데 도움이 됩니다.

📑 2-2. CD (Continuous Deployment & Continuous Delivery)

CD는 지속적 배포(Continuous Deployment)와 지속적 전달(Continuous Delivery) 두 가지 의미로 사용된다. 두 개념은 비슷하지만 약간의 차이가 있다. CD는 지속적 통합을 통과한 코드를 자동으로 빌드, 테스트, 그리고 스테이징 환경까지 배포하는 것으로, 제품을 언제든지 출시할 수 있는 상태로 유지하기 위해 적용된다. 그럼 지속적 배포와 지속적 전달의 차이는 뭘까?

✒️ 지속적 전달(Continuous Delivery)

지속적 전달은 개발된 소프트웨어를 언제든지 실제 프로덕션 환경에 배포할 수 있는 상태로 유지하는 것을 말한다. 즉, 모든 변경사항(신규 기능, 설정 변경, 버그 수정 등)이 테스트를 통과하고 배포 준비가 완료된 상태를 의미한다. 하지만 실제로 배포는 수동으로 실행되며, 이는 일반적으로 비즈니스 요구 사항이나 마케팅 전략에 따라 결정된다.

✒️ 지속적 배포(Continuous Deployment)

지속적 배포는 지속적 전달의 다음 단계로, 개발된 소프트웨어가 자동으로 실제 프로덕션 환경에 배포되는 것을 말한다. 즉, 모든 변경사항이 자동 테스트를 통과하면 즉시 사용자에게 제공된다. 이 방법은 개발 팀이 더 빠르게 피드백을 받을 수 있게 해주지만, 더 높은 수준의 테스트 자동화와 품질 보증이 필요하다.


3️⃣ 젠킨스의 특징과 장점

✒️ 다양한 환경에서의 활용

젠킨스는 Java로 작성되어 있어서, Java Runtime Environment(JRE)가 설치된 모든 운영 체제에서 실행할 수 있다. 이로 인해 Windows, Linux, macOS 등 다양한 플랫폼에서 동일한 방식으로 작동하며, 사용자의 운영 체제에 관계없이 적용할 수 있다. 또한, 웹 기반의 사용자 인터페이스를 통해 사용자가 쉽게 설정을 변경하고 작업을 관리할 수 있다.

✒️ 지원되는 언어 및 프레임워크

젠킨스는 다양한 프로그래밍 언어와 프레임워크를 지원한다. Java, C/C++, Python, PHP, JavaScript 등 다양한 언어뿐만 아니라, Maven, Ant, Gradle 등의 빌드 도구JUnit, Selenium 등의 테스트 도구를 지원한다. 이 외에도 Git, SVN 등의 버전 관리 시스템과 연동하여, 소스 코드의 변경을 자동으로 감지하고 빌드 및 테스트를 수행할 수 있다.

✒️ 다양한 플러그인 지원

젠킨스는 플러그인 아키텍처를 통해 사용자가 필요에 따라 기능을 추가하거나 확장할 수 있다. 1000개 이상의 플러그인이 제공되며, 이를 통해 사용자는 특정 기능을 추가하거나, 특정 도구와의 통합을 구현하거나, 특정 환경에 맞게 설정을 변경할 수 있다. 예를 들어, Docker, Kubernetes, AWS, Azure 등과 같은 클라우드 서비스와의 통합, Slack, Email 등과 같은 알림 시스템과의 통합 등을 통해, 사용자의 요구사항에 따라 매우 다양한 방식으로 젠킨스를 활용할 수 있다.


4️⃣ 젠킨스와 깃허브

젠킨스는 깃허브와의 통합을 통해 깃허브 레포지토리에서 발생하는 이벤트(예: 코드 푸시, 풀 리퀘스트 등)를 감지하고, 이를 트리거로하여 레포지토리에 있는 소스코드에 대한 빌드, 테스트, 배포 등의 작업을 자동으로 수행할 수 있다.

이를 위해 젠킨스에서는 GitHub 플러그인을 활용한다. GitHub 플러그인을 통해 젠킨스는 GitHub 레포지토리의 웹훅을 설정하여 실시간으로 이벤트를 수신할 수 있다. 해당 이벤트가 발생할 때마다 젠킨스는 미리 정의된 파이프라인을 실행한다. 이렇게 하면 소프트웨어 개발 및 배포 프로세스를 자동화하고 효율화할 수 있다.


📑 4-1. GitHub Actions와 Jenkins의 차이

GitHub Actions과 Jenkins는 모두 지속적 통합 및 지속적 전달을 지원하는 도구지만 몇 가지 차이점이 있다.

JenkinsGitHub Actions
호스팅독립적으로 호스팅되며, 사용자가 직접 서버를 설정하고 유지보수GitHub의 일부로 제공되어 GitHub 리포지토리에서 직접 관리
접근성독립적으로 운영GitHub 리포지토리에서 쉽게 사용
비용오픈 소스, 하지만 서버 및 인프라 구성이나 유지보수에 대한 비용이 발생할 수 있음무료 및 유료 플랜을 제공, 공개 리포지토리에서는 무료로 사용 가능
관리 및 유지보수사용자가 서버를 관리하고 유지보수해야 함서버 관리 및 유지보수에 대한 부담이 적음
플러그인플러그인, Jenkins 서버 내에서 플러그인이 작동하며, Jenkins의 기능을 확장하고 다양한 작업을 자동화함액션, 도커 컨테이너 내에서 각 액션이 독립적으로 작동하며, GitHub 리포지토리에서 이벤트를 트리거하고 워크플로우를 자동화함

📑 4-2. GitHub과의 통합 방법

  1. GitHub 리포지토리의 설정에서 Webhook을 추가하고, Payload URL에는 젠킨스 서버의 주소에 /github-webhook/을 추가한다.
  2. 젠킨스에서는 GitHub과의 연동을 위해 GitHub Plugin을 설치하고, 연동 시 사용할 인증 정보를 설정한다.
  3. 젠킨스 관리 페이지에서 '플러그인 관리'를 클릭하여 'GitHub 플러그인'을 설치한다.
  4. 젠킨스에서 새로운 작업을 생성하고, 소스 코드 관리 섹션에서 'Git'을 선택한다.
  5. GitHub 레포지토리의 URL을 입력한다.
  6. GitHub hook trigger for GITScm polling 옵션을 체크한다.
    • 젠킨스는 GitHub에서 PR, Push 등 특정 이벤트가 일어날 때 이를 트리거로 설정할 수 있다. GitHub hook trigger for GITScm polling 옵션은 GitHub에서의 변경사항(설정된 트리거)을 감지하고 젠킨스 작업을 수행하게 하는 설정이다.
    • 구체적인 트리거는 GitHub Plugin과 파이프라인을 통해 설정할 수 있다.

5️⃣ 젠킨스와 도커

젠킨스를 사용하여 배포 파일을 서버에 전송하고 실행하는 것은 충분히 가능하다. 하지만 이러한 작업을 수행하려면 실행 스크립트를 작성하고, 해당 스크립트를 실행하는 젠킨스 파이프라인을 구성해야 한다. 이 방법은 다소 불편할 수 있고, ****코드 수정이나 배포 환경의 변화에 따라 서버에 직접 접속해서 환경 설정을 변경해줘야 하는 불편함이 있을 수 있다.

따라서, 이러한 문제를 해결하기 위해 도커를 사용하는 경우가 많다. 도커를 사용하면 배포 환경을 코드로 관리할 수 있고, 이를 통해 환경 설정의 일관성을 보장하고 재사용성을 높일 수 있다. 또한, 젠킨스는 도커와 잘 통합되어 있어 도커를 활용한 CI/CD 파이프라인을 손쉽게 구축할 수 있다.


📑 5-1. 젠킨스와 깃허브, 도커를 이용한 배포 플로우 예시

아래는 직접 진행했던 프로젝트의 아키텍처를 가져온 것이다.

깃허브 레포지토리에 push가 일어나면 actions가 돌아가면서 테스트를 진행하고, 문제가 없을 경우 webhook을 통해 jenkins가 해당 업데이트를 가져온다. 이때 jenkins에는 미리 어떤 브랜치에 어떤 동작이 일어날 경우 깃허브에서 코드를 가져오는지 파이프라인으로 설정되어있다.
jenkins는 가져온 코드를 통해 파이프라인에 설정된대로 도커 이미지 파일을 만들어 도커 허브에 올리고, 도커 허브에서는 기존 컨테이너를 종료시키고 업데이트된 도커 이미지 파일을 실행한다.

이 전체 배포과정에서 개발자가 한 일은 github 브랜치에 push한 것 밖에 없다.

미리 파이프라인이나 다른 설정을 해줘야하긴하지만, 설정해주고 난 뒤에는 이 배포 과정에 관여를 하지 않아도 되기 떄문에 매우 편리하다.


6️⃣ 젠킨스의 파이프라인

그렇다면 위에서 언급한 파이프라인은 뭘까?
파이프라인은 말 그대로 파이프를 이어 붙인것과 같은 형태로 연속적인 작업들을 젠킨스에서 하나의 파이프라인(작업)으로 묶어서 관리할 수 있게 만드는 것을 말한다.

파이프라인은 다양한 단계(stage)로 구성되며, 각 단계는 특정 작업을 수행한다. 예를 들어, 소스 코드 체크아웃, 빌드, 테스트, 배포, 통합 테스트 등이 파이프라인의 각 단계가 될 수 있다. 각 단계에서 실행되는 작업은 사용자가 정의한 스크립트로 구성되며, 이러한 스크립트를 통해 작업의 세부 내용을 제어할 수 있다.

Jenkinsfile은 젠킨스 파이프라인의 정의를 담은 텍스트 파일이다. 즉 개발 및 배포 프로세스를 자동화하기 위한 코드화된 스크립트가 들어가있는 파일이며, 이 파일은 파이프라인을 구성하는 여러 단계를 정의하고, 이 단계들이 어떻게 실행되어야 하는지에 대한 로직을 포함합니다. 이 파일은 프로젝트의 소스 코드와 함께 버전 관리 시스템에 저장(일반적으로 프로젝트의 루트 디렉토리에 위치함)되므로, 파이프라인 설정도 코드로 관리하고 버전을 추적할 수 있다.


📑 6-1. 파이프라인의 특징과 장점

  • 다양한 단계를 정의
    파이프라인은 여러 개의 '스테이지(stage)'로 구성된다. 각 스테이지는 특정 작업(예: 빌드, 테스트, 배포)을 수행한다. 이렇게 여러 단계를 명확히 정의하고 구분함으로써 작업의 흐름을 쉽게 이해하고 관리할 수 있다.
  • 병렬 실행과 조건부 실행 지원
    파이프라인은 병렬 실행을 지원하여 작업의 실행 시간을 줄일 수 있다. 또한, 조건부 실행을 지원하여 특정 조건을 만족할 때만 특정 스테이지를 실행하도록 설정할 수 있다.
  • 재시작 지점 설정
    파이프라인은 '체크포인트(checkpoint)'를 설정하여 실패한 경우에도 특정 단계부터 재시작할 수 있다. 이는 오랜 시간이 걸리는 작업을 다시 실행하는 데 걸리는 시간을 줄일 수 있다.

📑 6-2. Jenkinsfile의 구현 방식

Jenkinsfile(파이프라인 스크립트)은 Groovy 언어를 기반으로 하며, 스크립티드 파이프라인 방식과 선언적 파이프라인 방식 두 가지 형태로 작성할 수 있다. 각각의 방법은 장단점이 있으므로, 특정 상황이나 선호도에 따라 선택하여 구현할 수 있다.

✒️ 6-2-1. Scripted Pipeline 스크립티드 파이프라인

초기 젠킨스 파이프라인의 형태로, Groovy 스크립트로 작성된다. 스크립티드 파이프라인은 보통 node 블록 내에서 정의된다. 복잡하고 유연한 빌드 및 배포 프로세스를 구현하려는 경우에 적합하다.

node {
  // 작업을 수행하는 스텝들...
  stage('Build') {
    // 빌드 스텝...
  }
  stage('Test') {
    // 테스트 스텝...
  }
  // 추가 스테이지...
}

✒️ 6-2-2. Declarative Pipeline 선언적 파이프라인

상대적으로 최근에 도입된 방식으로, 구조화된 형태의 Groovy DSL을 사용하여 파이프라인을 정의한다. 선언적 파이프라인은 pipeline 블록 내에서 정의된다.
선언적 파이프라인은 구조화된 형태를 가지므로 스크립티드 파이프라인에 비해 덜 복잡하다. 문법이 간단하고 읽기 쉽다.

pipeline {
  agent any
  stages {
    stage('Build') {
      steps {
        // 빌드 스텝...
      }
    }
    stage('Test') {
      steps {
        // 테스트 스텝...
      }
    }
    // 추가 스테이지...
  }
}

📑 6-3파이프라인 구성 예시 (선언적 파이프라인)

✒️ windows 기반

``` groovy
pipeline {
    agent any
    tools {
        jdk 'Java 11'
        gradle 'Gradle 8.3'
    }
    triggers {
				// github 저장소에 push 이벤트가 발생할 때마다 Jenkins 파이프라인이 실행되도록 설정
        githubPush()
    }
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        stage('Build') {
            steps {
                withCredentials([file(credentialsId: 'application-yml', variable: 'SECRETS_APPLICATION')]) {
                    script {
                        bat "copy %SECRETS_APPLICATION% C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\meta-mingle\\src\\main\\resources\\application.yml"
                    }
                }
                withCredentials([file(credentialsId: 'meta-mingle-firebase-key', variable: 'SECRETS_FIREBASE')]) {
                    script {
                        bat "copy %SECRETS_FIREBASE% C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\meta-mingle\\src\\main\\resources\\meta-mingle-firebase-key.json"
                    }
                }

                bat(script: 'gradlew clean build', returnStatus: true)
            }
        }
        stage('Deploy') {
            steps {
                withCredentials([
                    string(credentialsId: 'deploy-dir', variable: 'DEPLOY_DIR'),
                    string(credentialsId: 'docker-image-name', variable: 'DOCKER_IMAGE_NAME'),
                    string(credentialsId: 'docker-container-name', variable: 'DOCKER_CONTAINER_NAME'),
                    usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'DOCKERHUB_USERNAME', passwordVariable: 'DOCKERHUB_PASSWORD')]) {
                    script {

                        bat "copy /Y build\\libs\\meta-mingle-0.0.1-SNAPSHOT.jar %DEPLOY_DIR%"

                        bat "docker build -t %DOCKER_IMAGE_NAME% ."

                        bat "docker login -u %DOCKERHUB_USERNAME% -p %DOCKERHUB_PASSWORD%"

                        // 기존 컨테이너를 중지하고 제거
                        bat "docker stop %DOCKER_CONTAINER_NAME% || (echo Container not running or does not exist. & exit 0)"
                        bat "docker rm %DOCKER_CONTAINER_NAME% || (echo Container not running or does not exist. & exit 0)"

                        // DockerHub에 생성한 이미지 push
                        bat "docker push %DOCKER_IMAGE_NAME%"

                        // Docker 이미지로 새 컨테이너 실행
                        bat "docker run -d --name %DOCKER_CONTAINER_NAME% -p 8080:8080 %DOCKER_IMAGE_NAME%"
                    }
                }
            }
        }
    }
}
```

✒️ linux 기반

``` groovy
pipeline {
    agent any
    tools {
        jdk 'Java 11'
        gradle 'Gradle 8.3'
    }
    triggers {
				// Cron 표현식을 사용하여 SCM(Source Code Management)을 주기적으로 폴링하여 변경 사항이 있는지 확인. 
				// 아래의 표현식은 "매 3분마다 폴링"을 의미. 
				// 주기적인 폴링은 특정 이벤트(예: GitHub Push)가 발생하지 않더라도 정기적으로 소스 코드 변경을 감지하여 빌드를 수행한다.
        pollSCM('H/3 * * * *')
    }
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
            post {
                success {
                    echo 'Successfully checkouted epository'
                }
                failure {
                    error 'This pipeline stops here...'
                }
            }
        }
        stage('Build') {
            steps {
                withCredentials([file(credentialsId: 'application-yml', variable: 'SECRETS_APPLICATION')]) {
                    script {
                        sh "cp $SECRETS_APPLICATION /path/to/your/project/src/main/resources/application.yml"
                    }
                }
                withCredentials([file(credentialsId: 'meta-mingle-firebase-key', variable: 'SECRETS_FIREBASE')]) {
                    script {
                        sh "cp $SECRETS_FIREBASE /path/to/your/project/src/main/resources/meta-mingle-firebase-key.json"
                    }
                }

                sh './gradlew clean build'
            }
        }
        stage('Deploy') {
            steps {
                withCredentials([
                    string(credentialsId: 'deploy-dir', variable: 'DEPLOY_DIR'),
                    string(credentialsId: 'docker-image-name', variable: 'DOCKER_IMAGE_NAME'),
                    string(credentialsId: 'docker-container-name', variable: 'DOCKER_CONTAINER_NAME'),
                    usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'DOCKERHUB_USERNAME', passwordVariable: 'DOCKERHUB_PASSWORD')]) {
                    script {
                        sh "cp build/libs/meta-mingle-0.0.1-SNAPSHOT.jar $DEPLOY_DIR"

                        sh "docker build -t $DOCKER_IMAGE_NAME ."

                        sh "docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD"

                        // 기존 컨테이너를 중지하고 제거
                        sh "docker stop $DOCKER_CONTAINER_NAME || (echo Container not running or does not exist. & exit 0)"
                        sh "docker rm $DOCKER_CONTAINER_NAME || (echo Container not running or does not exist. & exit 0)"

                        // DockerHub에 생성한 이미지 push
                        sh "docker push $DOCKER_IMAGE_NAME"

                        // Docker 이미지로 새 컨테이너 실행
                        sh "docker run -d --name $DOCKER_CONTAINER_NAME -p 8080:8080 $DOCKER_IMAGE_NAME"
                    }
                }
            }
        }
    }
}
```

0개의 댓글