Docker와 Jenkins를 통한 CI/CD 구축(GIt Branch 전략 수정 전)

박세건·2024년 9월 10일
0

기술 실습

목록 보기
6/18
post-thumbnail

밑에 과정은 깃 브랜치 전략을 수정하기 전 단계로 모노레포형식으로 프로젝트를 진행했을때의 CI/CD 구축 방식이다.
하지만 자신이 담당하지 않는 부분의 코드를 갖고있어야한다는 점, CI/CD 진행시에 수정된 부분을 일일이 확인(이 확인하는 과정이 비효율적이라고 생각)하고 해당 디렉토리로 이동해서 CI/CD를 진행한다는 점을 해결하기위해서 깃 브랜치전략을 수정한 후에 이어서 다시 진행하였다.
이 글은 수정 전의 모노레포 구조의 CI/CD 방식으로 참고해서 읽길 바랍니다.


이전에 Jenkins를 사용해서 CI/CD를 구축해 PUSH와 Merge에 대해 Webhook을 걸어서 서비스가 배포되도록 진행했고 배포된 서버까지 정상적으로 작동되는 것을 확인했다.

이번에는 Docker를 이용해서 CI/CD를 적용시켜보려고한다.

왜 Docker를 사용했나

  • 환경 일관성:
    기본 CI/CD: 개발 환경과 배포 환경이 다를 수 있어, 배포 후 문제가 발생할 수 있습니다.
    도커 CI/CD: 도커 이미지를 사용하면, 개발 환경과 배포 환경이 동일하게 유지되어 일관성이 보장됩니다.
  • 의존성 관리:
    기본 CI/CD: 시스템에 설치된 라이브러리와 패키지에 의존하므로, 충돌이 발생할 수 있고 중요파일에 대한 설정이 노출될 가능성이 있습니다.
    도커 CI/CD: 모든 의존성이 도커 이미지 안에 포함되어 있어, 다른 환경에서도 동일하게 실행됩니다.
  • 스케일링:
    기본 CI/CD: 서버에 직접 배포하므로, 수동으로 확장해야 합니다.
    도커 CI/CD: 도커를 사용하면 컨테이너를 쉽게 복제하고 확장할 수 있습니다.

추가적인 MSA 구조에서의 도커 CI/CD 장점

프로젝트를 MSA 구조로 진행하기로 했고 도커를 통한 CI/CD를 구축하면 개발 및 운영 효율성을 크게 향상시킬 수 있는 장점을 갖고있어서 적용하려한다.

  • 독립적인 배포:
    각 마이크로서비스는 독립적으로 배포할 수 있어, 서비스 간의 의존성 문제를 최소화할 수 있습니다. 도커를 통해 각 서비스의 이미지를 관리하면, 특정 서비스만 업데이트할 수 있습니다.
  • 환경 일관성:
    도커를 사용하면 모든 마이크로서비스가 동일한 환경에서 실행되므로, 개발 환경과 운영 환경의 차이로 인한 문제가 줄어듭니다.
  • 스케일링 용이:
    각 마이크로서비스를 개별적으로 스케일링할 수 있어, 특정 서비스의 트래픽이 증가할 때만 해당 서비스의 인스턴스를 추가할 수 있습니다.
  • 의존성 관리:
    도커 이미지를 통해 각 마이크로서비스의 종속성을 명확하게 관리할 수 있습니다. 서비스 간의 라이브러리 충돌 문제를 피할 수 있습니다.

Docker를 이용한 CI/CD

CI/CD 과정

  1. Git에 있는 프로젝트를 Clone해, 빌드를 한다.
  2. jar 파일이 생성되고, 도커 이미지로 빌드한다.
  3. 도커 이미지를 도커 허브에 업로드 한다.
  4. 지금 도커에서 run 되고 있는 해당 이미지를 중지하고, 삭제시킨다.
  5. 도커 허브에 있던 새 이미지를 받아 run 한다.
  6. Spring 서버가 도커 컨테이너로 AWS에 배포된다.

Docker이미지를 위한 Dockerfile

빌드를진행하고 이를 Docker Image로 만들어주기 위해서는 Dockerfile을 작성해야한다. 마이크로서비스들을 Image화 할 수 있는 Dockerfile을 작성해보자

Dockerfile의 역할

  • 애플리케이션 환경 설정
    • 애플리케이션이 실행될 환경을 정의
  • 애플리케이션 코드 복사
    • 애플리케이션의 소스 코드를 복사
  • 명령어 실행
    • 컨테이너가 시작될 때 실행될 명령어를 작성
    • ex) java -jar
  • 이미지 빌드
    • docker build 명령어로 이미지 생성

파이프라인 Script

이전에 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 !'
                }
            }
        }
    }
}

Script 수정 사항

  • 프로젝트의 구조는 /backend 하위에 마이크로 서비스들이 존재하기에 step을 나눠주고 parallel을 사용해서 모든 마이크로서비스의 빌드를 동시에 진행하도록 한다.
  • MSA 구조를 갖기때문에 PUSH에 대한 Webhook이 발견되었을때에 모든 서비스에 대해 빌드와 이미지로 만들어주는 과정을 진행하는 것을 비효율
    • Commit 메시지를 통해서 어떤 파일이 변경되었는지를 확인
    • 변경된 파일의 주소는 backend/{서버이름}/{변경된 파일} 과 같은 형식으로 만들어질 것
    • 여기서 서버이름을 추출해서 배열(changedFiles)에 저장
    • 배열을 사용하기 위해서는 script문을 사용
    • startWith 메서드를 이용해서 앞단의 문장을 비교하여 어떤 마이크로서비스가 수정되었는지를 확인하고 서비스명을 다시 배열(servicesToBuild)에 저장
    • 다른 stage와의 데이터 공유를 하기위해서 환경변수를 사용해야하지만 환경변수는 문자열만 저장 가능
    • 배열정보를 문자열로 저장시키기 위한 과정 진행
    • 만들어진 문자열을 다시 해제하여 서비스 이름을 추출
    • 해당 서버에 맞는 디렉토리로 이동한 후에 Build 및 이미지 생성을 진행

만들어진 script

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 전략을 맞지 않다고 생각되어 수정후 다음단계를 다시 진행한다.

profile
멋있는 사람 - 일단 하자

0개의 댓글