밑에 과정은 깃 브랜치 전략을 수정하기 전 단계로 모노레포형식으로 프로젝트를 진행했을때의 CI/CD 구축 방식이다.
하지만 자신이 담당하지 않는 부분의 코드를 갖고있어야한다는 점, CI/CD 진행시에 수정된 부분을 일일이 확인(이 확인하는 과정이 비효율적이라고 생각)하고 해당 디렉토리로 이동해서 CI/CD를 진행한다는 점을 해결하기위해서 깃 브랜치전략을 수정한 후에 이어서 다시 진행하였다.
이 글은 수정 전의 모노레포 구조의 CI/CD 방식으로 참고해서 읽길 바랍니다.
이전에 Jenkins를 사용해서 CI/CD를 구축해 PUSH와 Merge에 대해 Webhook을 걸어서 서비스가 배포되도록 진행했고 배포된 서버까지 정상적으로 작동되는 것을 확인했다.
이번에는 Docker를 이용해서 CI/CD를 적용시켜보려고한다.
- 환경 일관성:
기본 CI/CD: 개발 환경과 배포 환경이 다를 수 있어, 배포 후 문제가 발생할 수 있습니다.
도커 CI/CD: 도커 이미지를 사용하면, 개발 환경과 배포 환경이 동일하게 유지되어 일관성이 보장됩니다.- 의존성 관리:
기본 CI/CD: 시스템에 설치된 라이브러리와 패키지에 의존하므로, 충돌이 발생할 수 있고 중요파일에 대한 설정이 노출될 가능성이 있습니다.
도커 CI/CD: 모든 의존성이 도커 이미지 안에 포함되어 있어, 다른 환경에서도 동일하게 실행됩니다.- 스케일링:
기본 CI/CD: 서버에 직접 배포하므로, 수동으로 확장해야 합니다.
도커 CI/CD: 도커를 사용하면 컨테이너를 쉽게 복제하고 확장할 수 있습니다.
프로젝트를 MSA 구조로 진행하기로 했고 도커를 통한 CI/CD를 구축하면 개발 및 운영 효율성을 크게 향상시킬 수 있는 장점을 갖고있어서 적용하려한다.
- 독립적인 배포:
각 마이크로서비스는 독립적으로 배포할 수 있어, 서비스 간의 의존성 문제를 최소화할 수 있습니다. 도커를 통해 각 서비스의 이미지를 관리하면, 특정 서비스만 업데이트할 수 있습니다.- 환경 일관성:
도커를 사용하면 모든 마이크로서비스가 동일한 환경에서 실행되므로, 개발 환경과 운영 환경의 차이로 인한 문제가 줄어듭니다.- 스케일링 용이:
각 마이크로서비스를 개별적으로 스케일링할 수 있어, 특정 서비스의 트래픽이 증가할 때만 해당 서비스의 인스턴스를 추가할 수 있습니다.- 의존성 관리:
도커 이미지를 통해 각 마이크로서비스의 종속성을 명확하게 관리할 수 있습니다. 서비스 간의 라이브러리 충돌 문제를 피할 수 있습니다.
CI/CD 과정
- Git에 있는 프로젝트를 Clone해, 빌드를 한다.
- jar 파일이 생성되고, 도커 이미지로 빌드한다.
- 도커 이미지를 도커 허브에 업로드 한다.
- 지금 도커에서 run 되고 있는 해당 이미지를 중지하고, 삭제시킨다.
- 도커 허브에 있던 새 이미지를 받아 run 한다.
- Spring 서버가 도커 컨테이너로 AWS에 배포된다.
빌드를진행하고 이를 Docker Image로 만들어주기 위해서는 Dockerfile을 작성해야한다. 마이크로서비스들을 Image화 할 수 있는 Dockerfile을 작성해보자
이전에 Docker를 사용하지 않았을때의 파이프라인 Script에서 clone부분은 동일하게 진행하고 Build 부분을 수정하려고한다.
기존 Script
pipeline {
agent any
stages {
stage('Clone') {
steps {
sh 'pwd'
git branch: 'dev', credentialsId: 'qkrtprjs', url: 'https://lab.ssafy.com/qkrtprjs456/test.git'
}
post {
failure {
echo 'Repository clone failure !'
}
success {
echo 'Repository clone success !'
}
}
}
stage('Build'){
steps{
sh 'pwd'
sh 'chmod +x gradlew' // 실행 권한 추가
sh './gradlew bootJar'
}
post {
failure {
echo 'Repository build failure !'
}
success {
echo 'Repository build success !'
}
}
}
stage('Deploy'){
steps {
dir('build/libs') {
sshPublisher(
publishers: [sshPublisherDesc(configName: 'test', transfers: [sshTransfer(cleanRemote: false, excludes: '',
execCommand:
'''
cd /home/ubuntu/jenkins_build
kill -9 `cat save_pid.txt`
rm save_pid.txt
nohup java -jar demo-0.0.1-SNAPSHOT.jar > logs/demo.log 2>&1 &
echo $! > save_pid.txt
''',
execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]
)
}
}
post {
failure {
echo 'Repository Deploy failure !'
}
success {
echo 'Repository Deploy success !'
}
}
}
}
}
pipeline {
agent any
stages {
stage('Clone') {
steps {
sh 'pwd'
git branch: 'develop', credentialsId: 'qkrtprjs', url: 'https://lab.ssafy.com/s11-fintech-finance-sub1/S11P21A604.git'
}
post {
failure {
echo 'Repository clone failure !'
}
success {
echo 'Repository clone success !'
}
}
}
stage('Check for Changes') {
steps {
script {
def changedFiles = sh(script: 'git diff --name-only HEAD^ HEAD', returnStdout: true).trim().split('\n')
def servicesToBuild = [] // Groovy 변수 선언
changedFiles.each { file ->
if (file.startsWith('backend/account/')) {
servicesToBuild << 'account'
} else if (file.startsWith('backend/chatBot/')) {
servicesToBuild << 'chatBot'
} else if (file.startsWith('backend/notification/')) {
servicesToBuild << 'notification'
} else if (file.startsWith('backend/payment/')) {
servicesToBuild << 'payment'
} else if (file.startsWith('backend/travel/')) {
servicesToBuild << 'travel'
} else if (file.startsWith('backend/user/')) {
servicesToBuild << 'user'
}
}
if (servicesToBuild.isEmpty()) {
error("No services to build.")
} else {
currentBuild.description = "Building services: ${servicesToBuild.join(', ')}"
env.SERVICES_TO_BUILD = servicesToBuild.join(',')
}
}
}
}
stage('Build Images') {
steps {
script {
def servicesToBuild = env.SERVICES_TO_BUILD.split(',').collect { it.trim() }
servicesToBuild.each { service ->
dir("backend/${service}") {
sh 'ls -al'
sh 'chmod +x ./gradlew'
sh './gradlew build'
sh "docker build -t qkrtprjs/${service} ."
}
echo "Built image for ${service}..."
}
}
}
}
// 주석 처리된 빌드 및 배포 단계
// stage('Build') {
// steps {
// sh 'pwd'
// sh 'chmod +x gradlew' // 실행 권한 추가
// sh './gradlew bootJar'
// }
// post {
// failure {
// echo 'Repository build failure !'
// }
// success {
// echo 'Repository build success !'
// }
// }
// }
// stage('Deploy') {
// steps {
// dir('build/libs') {
// sshPublisher(
// publishers: [sshPublisherDesc(configName: 'test', transfers: [sshTransfer(cleanRemote: false, excludes: '',
// execCommand: '''
// cd /home/ubuntu/jenkins_build
// kill -9 `cat save_pid.txt`
// rm save_pid.txt
// nohup java -jar demo-0.0.1-SNAPSHOT.jar > logs/demo.log 2>&1 &
// echo $! > save_pid.txt
// ''',
// execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '*.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]
// )
// }
// }
// post {
// failure {
// echo 'Repository Deploy failure !'
// }
// success {
// echo 'Repository Deploy success !'
// }
// }
// }
}
}
위 방식으로 진행하는데에 현재 진행하고있는 Branch 전략을 맞지 않다고 생각되어 수정후 다음단계를 다시 진행한다.