Skip to content
veronica2550 edited this page Oct 17, 2024 · 3 revisions

Jenkins CICD 구축 배경과 과정

0. 구축 배경 및 개요

프로젝트 초기에는 CI/CD 파이프라인이 전혀 적용되지 않은 상태였습니다. 코드가 수정되거나 기능이 추가될 때마다 수동으로 배포를 진행해야 했고, 이 과정은 시간이 많이 소요되었을 뿐만 아니라 사람이 개입하는 과정에서 휴먼에러가 발생할 가능성이 컸습니다.

개발 속도는 빨라지고, 빈번한 업데이트가 발생함에 따라 배포 과정의 효율화와 안정성을 확보할 필요가 있어서 JenkinsGitHub를 연동한 간단한 CI/CD 파이프라인을 구축하기로 했습니다.

cicd drawio

현재 이 프로젝트는 AWS EC2 인스턴스에서 컨테이너 없이 구동중인 프로젝트 구조와 배포 환경에 맞춰 적절히 구성된 파이프라인으로, 파이프라인을 통해 코드 푸시부터 운영 환경에 배포되는 과정이 자동화되었습니다.

1. 설치 전 백그라운드 실행 중인 프로세스 중지

실행되고 있는 process Id 찾기

ps aux | grep app.py

image (2)

출력에 관해 자세한 설명을 덧붙이자면, 다음과 같은 순서로 출력됩니다.

USER PID (Process ID) %CPU %MEM VSZ (Virtual Memory Size) RSS (Resident Set Size) TTY (Terminal) STAT (Process Status) START TIME COMMAND
ubuntu 343310 0.0 0.5 100444 87804 ? S Sep19 0:01 python3 app.py
ubuntu 362853 0.9 1.0 816836 166832 ? S1 Sep20 55:08 /home/ubuntu/.venv/bin/python3 app.py
ubuntu 418882 0.0 0.0 7084 2176 pts/8 S+ 12:13 0:00 grep --color=auto app.py
  • TTY (Terminal) 프로세스가 실행된 터미널을 나타냅니다. ?로 표시되면, 터미널과 연결되지 않은 데몬(백그라운드 프로세스)일 가능성이 큽니다.

  • STAT (Process Status) 프로세스의 상태를 나타내는 코드입니다. 주요 상태 코드는 다음과 같습니다:

    R: 실행 중 (Running)
    S: 대기 중 (Sleeping)
    D: 디스크 입출력을 기다리는 중 (Uninterruptible Sleep)
    Z: 좀비 프로세스 (Zombie)
    T: 일시 중지 (Stopped)
    

프로세스 종료하기

좌측에서 두 번째에 있는 숫자가 process Id 입니다.

 kill -9 362853

2. Jenkins 설치

Jenkins 공식 문서를 참고하였습니다.

  • Java : 17버전을 사용하였습니다.
  • 포트는sudo apt install default-jdk: 을 사용하였습니다.

자바 설치

  1. sudo apt update -y: 패키지 목록 업데이트
  2. sudo apt install default-jdk: JDK 설치
  3. java -version: 자바 버전 확인
    1. 출력
    openjdk version "17.0.12" 2024-07-16
    OpenJDK Runtime Environment (build 17.0.12+7-Ubuntu-1ubuntu224.04)
    OpenJDK 64-Bit Server VM (build 17.0.12+7-Ubuntu-1ubuntu224.04, mixed mode, sharing)
    

젠킨스 설치 | Long Term Support 릴리스

  1. sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
    https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key 
    Jenkins의 GPG 키를 다운로드하여 ‘/usr/share/keyrings/jenkins-keyring.asc’ 위치에 저장. 이 키는 Jenkins 저장소의 패키지가 신뢰할 수 있는 소스에서 온 것임을 확인하는 데 사용됩니다.
  2. echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc]" \
    https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
    /etc/apt/sources.list.d/jenkins.list > /dev/null 
    Jenkins 패키지 저장소를 추가. 추가된 저장소는 시스템이 Jenkins 패키지를 다운로드할 수 있도록 해줍니다..
  3. sudo apt-get update 
    추가된 Jenkins 저장소에서 사용 가능한 패키지 정보를 가져옵니다.
  4. sudo apt-get install jenkins
    Jenkins 설치.
  5. systemctl status jenkins
    Jenkins가 정상적으로 실행되고 있는지 확인합니다.
    q 눌러서 나감

3. Jenkins 셋업

Jenkins 기본 셋업

  1. http://3.34.165.197:8080/ 접속
    image
  2. sudo cat /var/lib/jenkins/secrets/initialAdminPassword
    위 화면 지시 사항 따름
    image
    터미널에 출력된 비밀번호 붙여넣기
  3. 플러그인 설치 방식 선택
    좌측: 권장 플러그인 모두 설치, 우측: 필요한 것만 골라서 설치
    Jenkins를 처음 사용해보기 때문에 권장 플러그인을 사용했습니다.
    image image
  4. 어드민 유저 생성
    image
  5. 젠킨스 URL 설정
    image
  6. 기본 set up 완료
    image

4. Webhook 세팅

Github에서 Webhook 설정 Repository에서 Settings - Webhooks - Add webhook을 클릭합니다.

Payload URL http://3.34.165.197:8080/github-webhook/

Content type application/json

Which events would you like to trigger this webhook? Just the push event

Active 선택

Add webhook 클릭

결과

image

Jenkins에서 Github Integration 플러그인 설치

이후 Webhook이 활성화 됩니다.

image

5. Workspace 설정

Jenkins 기본 workspace

/var/lib/jenkins/workspace/

item을 생성하게 되면 위 경로에 item의 이름으로 디렉토리가 생기고 그 안에서 빌드 작업이 일어나게 됩니다. 운영에 요청받은 workspace는 /home/ubuntu 이기 때문에, 이에 따른 새로운 적용 방식이 필요했습니다.

image

위와 같이 jenkins 파이프라인으로 생성된 item의 경우 권한이 jenkins 그룹의 jenkins에게 소유권이 있음을 확인할 수 있습니다. 또 Ubuntu는 750 권한을 가지며 Jenkins는 755 권한을 가집니다.

이에 따라 직관적으로 적용 가능한 방식은 아래와 같았습니다.

  1. /var/lib/jenkins/config.xml에서 <worksapceDir></workspaceDir> 내에 경로를 수정한다.
  2. /home/ubuntu에 대한 소유권을 jenkins로 이관한다.
  3. /home/ubuntu의 rwx 권한을 777로 바꾸거나, 파이프라인에 sudo 코드를 포함하여 jenkins의 권한 문제를 없앤다.

하지만 위 방법 모두 적합하지 않았습니다.

  1. workspace 자체를 바꾸는 경우, item 생성시 workspace 경로에 item의 이름으로 추가적인 디렉토리가 생기기 때문에, 프로젝트를 /home/ubuntu에서 바로 관리하기에 적합하지 않았습니다.
  2. 주로 pem 키를 활용하여 vsc로 접속하는 방식을 사용하는데, 소유권을 jenkins로 바꾸는 경우, SSH 보안 정책에 따라 홈 디렉토리와 키 파일의 권한이 올바르지 않으면 접속이 차단되기에 적합하지 않았습니다.
  3. 모든 권한을 부여하거나, sudo 코드를 사용하는 것은 보안적으로 적합하지 않았습니다.

최종적으로 선택한 방식은 아래와 같습니다.

Access Control List를 활용해 소유자와 소유 그룹에 따른 접근 권한 관리 방식에 추가로 임의의 사용자나 그룹에 접근 권한을 부여하는 것입니다.

엑세스 제어 목록 관리를 위해 라이브러리를 설치합니다.

 sudo apt install acl

setfacl 명령을 사용하여 /home/ubuntu 디렉토리와 그 하위 디렉토리 전체에 대해(-R 옵션) jenkins 사용자에게 읽기(r), 쓰기(w), 실행(x) 권한을 부여하는 명령입니다.

 sudo setfacl -R -m u:jenkins:rwx /home/ubuntu

이 외에 item 폴더는 755 권한을 지원해야하기 때문에, /home/ubuntu 디렉토리와 그 하위 디렉토리 전체에 755권한을 부여합니다.

 sudo chmod -R 755 /home/ubuntu

6. Pipeline

전체 코드

pipeline {
    agent any
    triggers {
        githubPush()
    }
    stages {
        stage('Debug') {
            steps {
                echo "Build Cause: ${currentBuild.getBuildCauses()}"
            }
        }
        stage('Build Trigger for Main Branch') {
            steps {
                git url: "https://github.com/wellcheck-AI/llm_AnswerGen.git",
                branch: "main"
            }
        }
        stage('Clone or Pull repository') {
            steps {
                script {
                    sh '''
                    #!/bin/bash
                    cd /home/ubuntu
                    if [ -d ".git" ]; then
                        echo "Repository exists, pulling latest changes from main branch..."
                        git config --global --add safe.directory /home/ubuntu
                        git checkout main
                        git pull origin main
                    else
                        echo "Repository does not exist, cloning main branch..."
                        git config --global --add safe.directory /home/ubuntu
                        git config --global init.defaultBranch main
                        git init
                        git remote add origin https://github.com/wellcheck-AI/llm_AnswerGen.git
                        git pull origin main
                    fi
                    '''
                }
            }
        }
        stage('Install dependencies') {
            steps {
                script {
                    sh '''
                    #!/bin/bash
                    cd /home/ubuntu
                    echo "현재 작업 디렉토리: $(pwd)"

                    VENV_DIR="./.venv"
                    
                    # 가상환경이 활성화되어 있는지 확인
                    if [ "$VIRTUAL_ENV" != "" ]; then
                        echo "가상환경이 이미 활성화되어 있습니다."
                    else
                        # 가상환경이 존재하는지 확인
                        if [ -d "$VENV_DIR" ] && [ -f "$VENV_DIR/bin/activate" ]; then
                            echo "가상환경을 활성화합니다."
                            . "$VENV_DIR/bin/activate"
                        else
                            echo "가상환경이 존재하지 않습니다. 새 가상환경을 생성합니다."
                            python3 -m venv "$VENV_DIR"  # 가상환경 생성
                            echo "가상환경을 활성화합니다."
                            . "$VENV_DIR/bin/activate"
                        fi
                    fi
                    
                    # 의존성 설치
                    pip install -r requirements.txt
                    '''
                }
            }
        }
        stage('Restart Flask App') {
            steps {
                script {
                    sh '''
                    cd /home/ubuntu
                    . ./.venv/bin/activate
                    # 기존 Flask 앱 종료
                    pkill -f "python3 app.py" || true  # 기존 프로세스가 없으면 오류 방지
                    # Flask 앱 다시 시작
                    JENKINS_NODE_COOKIE=dontKillMe && nohup python3 app.py > output.log 2>&1 &
                    '''
                }
            }
        }
    }
    post {
        success {
            echo 'Pipeline completed successfully!'
        }
        failure {
            echo '>>>>>>>>>>>>>Pipeline failed.<<<<<<<<<<<<<'
        }
        aborted {
            echo 'Pipeline was aborted'
        }
        always {
            cleanWs()
            echo 'Workspace cleaned.'
        }
    }
}

코드 분해

GitHub에서의 푸시 이벤트를 감지하여 Jenkins 빌드를 트리거합니다.
GUI 환경에서의 GitHub hook trigger for GITScm polling 옵션 체크와 동일합니다.

triggers {
    githubPush()
}

빌드 트리거가 어디에서, 누구에 의해 발생한 것인지 출력합니다.

stage('Debug') {
    steps {
        echo "Build Cause: ${currentBuild.getBuildCauses()}"
    }
}

Jenkins가 main 브랜치의 최신 코드를 GitHub 저장소에서 가져오는 코드입니다. 해당 파이프라인에서는 main branch에 대한 트리거를 설정하는 용도로 사용하고 있습니다. 이 코드로 이제 빌드 트리거는 main 브랜치 변경 시에만 발생합니다. 이 단계를 생략하면 파이프라인 트리거가 되지 않습니다.

코드는 Jenkins 기본 workspace에 clone됩니다.(파이프라인 마지막에 workspace 정리를 통해 관리되고 있습니다.)

stage('Build Trigger for Main Branch') {
    steps {
        git url: "https://github.com/wellcheck-AI/llm_AnswerGen.git",
        branch: "main"
    }
}

Github 레포지토리로부터 코드를 가져오는 과정입니다

.git 디렉토리의 존재를 확인 후 clone 혹은 pull 작업을 선택합니다.

두 작업 모두 /home/ubuntu 디렉토리를 Git의 안전한 작업 공간으로 추가하여, 이 디렉토리 내에서 Git 작업 시 보안 경고를 받지 않도록 설정합니다.

Clone 과정 git clone 명령어를 사용시 /home/ubuntu/ 환경 내 존재하는 디렉토리 및 폴더로 인해 명령어가 제대로 작동하지 않는 문제가 있었습니다. 그렇기 때문에 우회하는 방법을 선택하였습니다. git init 명령어를 통해 /home/ubuntu/를 Git 리포지토리로 변환하고, git remote add 명령어를 통해 원격 저장소 추가한 후에, git pull origin main 명령어로 main에 존재하는 코드를 받아오도록 구성하였습니다.

Pull 과정
EC2에 수동으로 Clone 하거나 Jenkins pipeline을 빌드했을 때 모두 main 브랜치가 생기기 때문에 main 브랜치로 checkout 합니다.
하지만 Jenkins pipeline로 빌드로 시작하는 경우 현재 브랜치가 원격 브랜치와 연결되어 있지 않아 git pull 만 사용하는 것은 불가하기 때문에 브랜치명을 명시해 원격지에서 코드를 받아옵니다.

stage('Clone or Pull repository') {
    steps {
        script {
            sh
            '''
            #!/bin/bash
            cd /home/ubuntu
            if [ -d ".git" ]; then
                echo "Repository exists, pulling latest changes from main branch..."
                git config --global --add safe.directory /home/ubuntu
                git checkout main
                git pull origin main
            else
                echo "Repository does not exist, cloning main branch..."
                git config --global --add safe.directory /home/ubuntu
                git config --global init.defaultBranch main
                git init
                git remote add origin https://github.com/wellcheck-AI/llm_AnswerGen.git
                git pull origin main
            fi
            '''
        }
    }
}

가상환경을 활성화하는 과정입니다. 모든 프로젝트에 대해 가상환경 이름을 .venv로 가정하고 있습니다.

가상환경의 활성화 검사 $VIRTUAL_ENV라는 환경 변수는 Python 가상환경이 활성화될 때 가상환경의 경로로 자동으로 설정됩니다. 빈 문자열이 아닌지 검사를 통해 활성화 유무를 판단합니다.

가상환경 존재 확인 가상환경이 비활성화 되어있는 경우 workspace에 .venv 디렉토리가 있는지, .venv/bin/activate 파일이 존재하는지 확인한 후 존재하지 않는 경우 가상환경을 생성합니다. 이후 두 경우 모두 가상환경을 활성화합니다.

stage('Install dependencies') {
     steps {
         script {
            sh
            '''
            #!/bin/bash
            cd /home/ubuntu
            echo "현재 작업 디렉토리: $(pwd)"
             
            VENV_DIR="./.venv"
             
            #가상환경이 활성화되어 있는지 확인
             if [ "$VIRTUAL_ENV" != "" ]; then
                 echo "가상환경이 이미 활성화되어 있습니다."
             else
                 # 가상환경이 존재하는지 확인
                 if [ -d "$VENV_DIR" ] && [ -f "$VENV_DIR/bin/activate" ]; then
                     echo "가상환경을 활성화합니다."
                     . "$VENV_DIR/bin/activate"
                 else
                    echo "가상환경이 존재하지 않습니다. 새 가상환경을 생성합니다."
                     python3 -m venv "$VENV_DIR"  # 가상환경 생성
                     echo "가상환경을 활성화합니다."
                     . "$VENV_DIR/bin/activate"
                 fi
             fi
             # 의존성 설치
             pip install -r requirements.txt
             '''
         }
     }
}

Flask APP을 재시작하는 부분입니다. 가상환경을 활성화한 후, 기존 프로세스를 중지합니다. 기존 프로세스가 없는 경우 에러 방지를 위해 || true를 추가하였습니다.

Jenkins 파이프라인에서는 보통 작업이 끝난 후 생성된 프로세스를 제거합니다. 따라서 JENKINS_NODE_COOKIE=dontKillMe 명령어를 통해 프로세스를 중단하지 않게 설정합니다.

stage('Restart Flask App') {
     steps {
         script {
             sh '''
             cd /home/ubuntu
             . ./.venv/bin/activate
             # 기존 Flask 앱 종료
             pkill -f "python3 app.py" || true  # 기존 프로세스가 없으면 오류 방지
             # Flask 앱 다시 시작
             JENKINS_NODE_COOKIE=dontKillMe && nohup python3 app.py > output.log 2>&1 &
             '''
         }
     }
}

post 블록은 파이프라인의 마지막에 실행되는 후처리 단계입니다.
현재, 파이프라인 전 과정의 성공, 실패, 중단, 항상 상태에 따라 관리하고 있습니다.

작업 공간 정리(cleanWs()) 기능을 통해 임시 파일과 빌드 산출물을 매번 제거합니다. 이 정리 작업을 통해 불필요한 메모리 점유를 방지하고 시스템 자원을 효율적으로 관리하고 있습니다.

post {
        success {
            echo 'Pipeline completed successfully!'
        }
        failure {
            echo '>>>>>>>>>>>>>Pipeline failed.<<<<<<<<<<<<<'
        }
        aborted {
            echo 'Pipeline was aborted'
        }
        always {
            cleanWs()
            echo 'Workspace cleaned.'
        }
    }

7. 정리 및 고도화 사항

빌드 결과 알림 - 파이프라인 프로세스 완료 후 해당 진행 상황은 오직 Jenkins 콘솔 출력에서만 확인할 수 있습니다. SMTP를 활용하여 이메일을 전송하거나, Slack API를 활용하여 메세지를 전송한다면 진행상황을 더욱 수월하게 파악할 수 있을 것입니다.

프로세스 PID 파일로 관리 - 현재 Flask 앱의 프로세스를 실행한 명령어 기반으로 찾고 있습니다. 더 명확하게 프로세스를 종료하는 방식으로 PID 파일을 사용할 수 있습니다. 앱을 실행할 때 PID 파일을 생성하고, 종료 시 해당 파일을 참조하여 프로세스를 종료하는 방법입니다.

Generic Webhook Trigger 라이브러리 활용 - 현재 구축한 파이프라인은 라이브러리 의존성이 낮아 유지보수성과 안정성이 높습니다. 이러한 기반 위에 Generic Webhook Trigger 라이브러리를 활용하여 파이프라인의 유연성과 자동화를 더욱 개선할 수 있습니다.