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

사진에서 파란색 부분이 CI, 노란색 부분이 CD에 해당한다.
지속적 통합과 지속적 전달, CI/CD는 소프트웨어 개발 및 배포 프로세스를 지속적으로 통합하고 제공하는 개발 방법론을 말한다.
CI는 여러 개발자들이 동시에 작업하고 있는 경우, 각자의 변경사항을 중앙 저장소에 지속적으로 통합하는 프로세스를 의미한다. CI는 팀 내의 여러 명의 개발자가 작성한 코드를 자주 통합하여 품질을 지속적으로 검증하기 위해 적용된다. 아래는 CI의 과정은 간단하게 정리한 것이다.

CD는 지속적 배포(Continuous Deployment)와 지속적 전달(Continuous Delivery) 두 가지 의미로 사용된다. 두 개념은 비슷하지만 약간의 차이가 있다. CD는 지속적 통합을 통과한 코드를 자동으로 빌드, 테스트, 그리고 스테이징 환경까지 배포하는 것으로, 제품을 언제든지 출시할 수 있는 상태로 유지하기 위해 적용된다. 그럼 지속적 배포와 지속적 전달의 차이는 뭘까?
지속적 전달은 개발된 소프트웨어를 언제든지 실제 프로덕션 환경에 배포할 수 있는 상태로 유지하는 것을 말한다. 즉, 모든 변경사항(신규 기능, 설정 변경, 버그 수정 등)이 테스트를 통과하고 배포 준비가 완료된 상태를 의미한다. 하지만 실제로 배포는 수동으로 실행되며, 이는 일반적으로 비즈니스 요구 사항이나 마케팅 전략에 따라 결정된다.
지속적 배포는 지속적 전달의 다음 단계로, 개발된 소프트웨어가 자동으로 실제 프로덕션 환경에 배포되는 것을 말한다. 즉, 모든 변경사항이 자동 테스트를 통과하면 즉시 사용자에게 제공된다. 이 방법은 개발 팀이 더 빠르게 피드백을 받을 수 있게 해주지만, 더 높은 수준의 테스트 자동화와 품질 보증이 필요하다.
젠킨스는 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 등과 같은 알림 시스템과의 통합 등을 통해, 사용자의 요구사항에 따라 매우 다양한 방식으로 젠킨스를 활용할 수 있다.
젠킨스는 깃허브와의 통합을 통해 깃허브 레포지토리에서 발생하는 이벤트(예: 코드 푸시, 풀 리퀘스트 등)를 감지하고, 이를 트리거로하여 레포지토리에 있는 소스코드에 대한 빌드, 테스트, 배포 등의 작업을 자동으로 수행할 수 있다.
이를 위해 젠킨스에서는 GitHub 플러그인을 활용한다. GitHub 플러그인을 통해 젠킨스는 GitHub 레포지토리의 웹훅을 설정하여 실시간으로 이벤트를 수신할 수 있다. 해당 이벤트가 발생할 때마다 젠킨스는 미리 정의된 파이프라인을 실행한다. 이렇게 하면 소프트웨어 개발 및 배포 프로세스를 자동화하고 효율화할 수 있다.
GitHub Actions과 Jenkins는 모두 지속적 통합 및 지속적 전달을 지원하는 도구지만 몇 가지 차이점이 있다.
| Jenkins | GitHub Actions | |
|---|---|---|
| 호스팅 | 독립적으로 호스팅되며, 사용자가 직접 서버를 설정하고 유지보수 | GitHub의 일부로 제공되어 GitHub 리포지토리에서 직접 관리 |
| 접근성 | 독립적으로 운영 | GitHub 리포지토리에서 쉽게 사용 |
| 비용 | 오픈 소스, 하지만 서버 및 인프라 구성이나 유지보수에 대한 비용이 발생할 수 있음 | 무료 및 유료 플랜을 제공, 공개 리포지토리에서는 무료로 사용 가능 |
| 관리 및 유지보수 | 사용자가 서버를 관리하고 유지보수해야 함 | 서버 관리 및 유지보수에 대한 부담이 적음 |
| 플러그인 | 플러그인, Jenkins 서버 내에서 플러그인이 작동하며, Jenkins의 기능을 확장하고 다양한 작업을 자동화함 | 액션, 도커 컨테이너 내에서 각 액션이 독립적으로 작동하며, GitHub 리포지토리에서 이벤트를 트리거하고 워크플로우를 자동화함 |
/github-webhook/을 추가한다.GitHub Plugin을 설치하고, 연동 시 사용할 인증 정보를 설정한다.GitHub hook trigger for GITScm polling 옵션을 체크한다. GitHub hook trigger for GITScm polling 옵션은 GitHub에서의 변경사항(설정된 트리거)을 감지하고 젠킨스 작업을 수행하게 하는 설정이다.
젠킨스를 사용하여 배포 파일을 서버에 전송하고 실행하는 것은 충분히 가능하다. 하지만 이러한 작업을 수행하려면 실행 스크립트를 작성하고, 해당 스크립트를 실행하는 젠킨스 파이프라인을 구성해야 한다. 이 방법은 다소 불편할 수 있고, ****코드 수정이나 배포 환경의 변화에 따라 서버에 직접 접속해서 환경 설정을 변경해줘야 하는 불편함이 있을 수 있다.
따라서, 이러한 문제를 해결하기 위해 도커를 사용하는 경우가 많다. 도커를 사용하면 배포 환경을 코드로 관리할 수 있고, 이를 통해 환경 설정의 일관성을 보장하고 재사용성을 높일 수 있다. 또한, 젠킨스는 도커와 잘 통합되어 있어 도커를 활용한 CI/CD 파이프라인을 손쉽게 구축할 수 있다.
아래는 직접 진행했던 프로젝트의 아키텍처를 가져온 것이다.
깃허브 레포지토리에 push가 일어나면 actions가 돌아가면서 테스트를 진행하고, 문제가 없을 경우 webhook을 통해 jenkins가 해당 업데이트를 가져온다. 이때 jenkins에는 미리 어떤 브랜치에 어떤 동작이 일어날 경우 깃허브에서 코드를 가져오는지 파이프라인으로 설정되어있다.
jenkins는 가져온 코드를 통해 파이프라인에 설정된대로 도커 이미지 파일을 만들어 도커 허브에 올리고, 도커 허브에서는 기존 컨테이너를 종료시키고 업데이트된 도커 이미지 파일을 실행한다.
이 전체 배포과정에서 개발자가 한 일은 github 브랜치에 push한 것 밖에 없다.
미리 파이프라인이나 다른 설정을 해줘야하긴하지만, 설정해주고 난 뒤에는 이 배포 과정에 관여를 하지 않아도 되기 떄문에 매우 편리하다.

그렇다면 위에서 언급한 파이프라인은 뭘까?
파이프라인은 말 그대로 파이프를 이어 붙인것과 같은 형태로 연속적인 작업들을 젠킨스에서 하나의 파이프라인(작업)으로 묶어서 관리할 수 있게 만드는 것을 말한다.
파이프라인은 다양한 단계(stage)로 구성되며, 각 단계는 특정 작업을 수행한다. 예를 들어, 소스 코드 체크아웃, 빌드, 테스트, 배포, 통합 테스트 등이 파이프라인의 각 단계가 될 수 있다. 각 단계에서 실행되는 작업은 사용자가 정의한 스크립트로 구성되며, 이러한 스크립트를 통해 작업의 세부 내용을 제어할 수 있다.
Jenkinsfile은 젠킨스 파이프라인의 정의를 담은 텍스트 파일이다. 즉 개발 및 배포 프로세스를 자동화하기 위한 코드화된 스크립트가 들어가있는 파일이며, 이 파일은 파이프라인을 구성하는 여러 단계를 정의하고, 이 단계들이 어떻게 실행되어야 하는지에 대한 로직을 포함합니다. 이 파일은 프로젝트의 소스 코드와 함께 버전 관리 시스템에 저장(일반적으로 프로젝트의 루트 디렉토리에 위치함)되므로, 파이프라인 설정도 코드로 관리하고 버전을 추적할 수 있다.
Jenkinsfile(파이프라인 스크립트)은 Groovy 언어를 기반으로 하며, 스크립티드 파이프라인 방식과 선언적 파이프라인 방식 두 가지 형태로 작성할 수 있다. 각각의 방법은 장단점이 있으므로, 특정 상황이나 선호도에 따라 선택하여 구현할 수 있다.
초기 젠킨스 파이프라인의 형태로, Groovy 스크립트로 작성된다. 스크립티드 파이프라인은 보통 node 블록 내에서 정의된다. 복잡하고 유연한 빌드 및 배포 프로세스를 구현하려는 경우에 적합하다.
node {
// 작업을 수행하는 스텝들...
stage('Build') {
// 빌드 스텝...
}
stage('Test') {
// 테스트 스텝...
}
// 추가 스테이지...
}
상대적으로 최근에 도입된 방식으로, 구조화된 형태의 Groovy DSL을 사용하여 파이프라인을 정의한다. 선언적 파이프라인은 pipeline 블록 내에서 정의된다.
선언적 파이프라인은 구조화된 형태를 가지므로 스크립티드 파이프라인에 비해 덜 복잡하다. 문법이 간단하고 읽기 쉽다.
pipeline {
agent any
stages {
stage('Build') {
steps {
// 빌드 스텝...
}
}
stage('Test') {
steps {
// 테스트 스텝...
}
}
// 추가 스테이지...
}
}
``` 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%"
}
}
}
}
}
}
```
``` 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"
}
}
}
}
}
}
```