[Server] Jenkins + Docker를 이용한 CI/CD 배포

김강욱·2024년 6월 15일
0

Project-Evertrip

목록 보기
19/19
post-thumbnail

이번 포스팅에서는 JenkinsDocker를 사용하여 Evertrip 프로젝트를 CI/CD 배포하는 시간을 가져보도록 하겠습니다.

Github Actions를 이용한 CI/CD는 아래 포스팅을 참고하시면 좋을 것 같습니다.

[Server] Github Actions + Docker를 이용한 CI/CD 배포


🙌 왜 Jenkins, Docker를 선택하였나요?

여러 가지 CI/CD 관련 툴 중에서 AWS EC2 서버를 이용해 무료로 사용할 수 있으며, 다른 툴에 비해 활용 사례와 레퍼런스가 다양한 Jenkins를 선택하였습니다.

또한 Docker를 사용한 이유는 OS 환경에 종속적이지 않도록 하기 위함이었습니다. 로컬 서버는 윈도우 OS를 사용하였지만 배포 서버는 ubuntu OS였기에 애플리케이션을 OS 환경에 종속적이지 않게 Docker를 활용하여 애플리케이션 이미지를 빌드하고 Docker hub저장소에 올리고 배포 서버에서 해당 이미지를 다운받고 실행하도록 구현하였습니다.

🙌 CI/CD 적용하기

이제 실제 CI/CD를 구축해보도록 하겠습니다. JenkinsGithub Actions와 다르게 Github와 직접적으로 연동되어 있지 않기 때문에 Github의 코드 변동 사항이 발생할 경우 Jenkins에 알리기 위해 부가적인 설정을 따로 해줘야합니다.

아래와 같은 순서로 진행해보도록 하겠습니다.

  1. GitHub Repository에서 Token 발급
  2. Jenkins 플러그인 설치
  3. Jenkins Credentials 생성
  4. Jenkins Pipeline 설정
  5. Jenkins 환경 변수 설정
  6. Jenkinsfile 작성
  7. GitHub Repository Webhooks 설정
  8. 테스트 해보기

🎈 1. GitHub Repository에서 Token 발급

먼저 Github Repository에서 Token을 발급 받아야 합니다. 해당 Token을 이용하여 Jenkins에서 Github Repository에 접근하여 코드를 가져오고 빌드 과정을 수행하게 됩니다.

Settings -> Developer settings -> Personal access tokens -> Generate new token (classic)를 선택하셔서 Token을 발급 받습니다.

repo, admin:org, admin:repo_hook을 선택해줍니다. 그리고 발급 받은 Token은 따로 저장해줍니다.

🎈 2. Jenkins Plugin 설치

다음은 Jenkins에서 GithubDockerSSH 접속 관련 플러그인을 설치하여야 합니다.

  • Generic Integration
  • Generic Webhook Trigger
  • GitHub API Plugin
  • Docker Pipeline
  • SSH Agent Plugin




🎈 3. Jenkins Credentials 생성

다음은 Jenkins Credentials 생성입니다. Jenkins 관리 -> Manage Credentials를 클릭하시고 Stores scoped to Jenkins > global > Add Credntials 선택하여 생성해주시면 됩니다.

각각 Docker hub, SSH, GitHub에 접근하기 위한 세 가지의 Credentials로 생성하였습니다.

1. Docker hub

먼저 Docker hub에 접근하기 위한 Credentials를 생성하겠습니다. Docker hub 사이트에 접속하여 로그인 후 아래와 같이 Account settings를 클릭합니다.

Security 태그에 들어가셔서 New Access Token을 클릭해줍니다.

Read,Write,Delete 권한을 설정한 후 Access Token을 발급 받습니다.

이후 아래와 같이 JenkinsCredentials를 생성해줍니다. Password에 위에서 발급 받은 Token을 넣으시면 됩니다.

2. SSH

다음으로 접근하려는 EC2 서버의 SSH 인증 정보를 Jenkins에 등록해줍니다.

Kind에서 SSH Username with private Key를 선택하시고 ID를 입력하신 후 Username은 접근하려는 EC2 서버의 사용자 이름을 기입하시면 됩니다. Private Key에는 SSH의 Private Key인 pem 파일의 전체 내용을 넣어주시면 됩니다.

3. Github

다음으로 Gitgub Repository에 접근할 수 있도록 아래의 Credentials를 생성해줍니다.

Kind는 Username with password로 선택해주시고 Username에는 Github의 ID를 넣어주시면 됩니다. 위에서 발급 받은 Github Access Token 값을 Password에 기입해주시면 됩니다.

🎈 4. Jenkins Pipeline 설정

다음은 Jenkins Pipeline을 설정해보도록 하겠습니다. 새로운 아이템 -> Pipeline으로 생성하시면 됩니다.

General -> GitHub project -> Project url에서 프로젝트의 GitHub Repository 경로를 기입해줍니다.

Build Triggers 탭에서 GitHub hook trigger for GITScm polling을 체크해줍니다.

Pipeline 탭에서 아래와 같이 설정해줍니다. Repository URL에 프로젝트의 GitHub Repository 경로를 기입하고 Credentials에 위에서 생성한 Github Credentials를 선택해줍니다.

Branch Specifier에서 트리거가 될 브랜치를 선택해줍니다. 저희 프로젝트에서는 main 브랜치에 코드 변경 사항이 생길 경우 Jenkins에 알려주기 위해 */main으로 설정해주었습니다.

마지막으로 Script Path에서 루트 프로젝트에서 Jenkinsfile의 경로를 기입해줍니다.

🎈 5. Jenkins 환경변수 설정

다음은 Jenkins에서 사용할 환경변수를 설정해보도록 하겠습니다.

Jenkins 관리 -> System -> Global properties에서 설정할 수 있습니다. 저는 EC2 서버 호스트 주소와 Jasypt 비밀번호를 생성하였습니다.

🎈 6. Jenkinsfile 작성

다음은 Jenkinsfile을 작성해보도록 하겠습니다. GitHub Webhook을 통해 Jenkins에 코드 변경 사항에 대한 알림이 왔을 때 해당 Jenkinsfile이 실행되게 됩니다. 루트 디렉토리 경로 하위에 Jenkinsfile을 작성해주었습니다.

pipeline {
    agent any
    tools {
        jdk 'JDK'
    }
    
    environment {
        SPRING_PROFILES_ACTIVE = 'prod'
        AWS_METADATA_DISABLED = 'true'
        JASYPT_PASSWORD = "${JASYPT_PASSWORD}"
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/evertrip/evertrip-backend.git'
            }
        }
        
        stage('Set Permissions') {
            steps {
                sh 'chmod +x gradlew'
            }
        }
       
        stage('Build and Test') {
            steps {
                    sh '''
                    echo "JASYPT_PASSWORD=${JASYPT_PASSWORD}"
                    ./gradlew clean build --info --stacktrace -Djasypt.encryptor.password=${JASYPT_PASSWORD} -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} -Dcom.amazonaws.sdk.disableEc2Metadata=${AWS_METADATA_DISABLED}
                    '''

            }
        }

        stage('Build Docker Image') {
            steps {
                script {
                    def image = docker.build('rlarkddnr1686/evertrip-image:latest')
                }
            }
        }

        stage('Push Docker Image') {
            steps {
                withCredentials([usernamePassword(credentialsId: 'dockerhub-jenkins', usernameVariable: 'DOCKERHUB_USERNAME', passwordVariable: 'DOCKERHUB_PASSWORD')]) {
                script {
                    docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-jenkins') {
                         sh '''
                             docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD
                             docker tag rlarkddnr1686/evertrip-image:latest rlarkddnr1686/evertrip-image:latest
                             docker push rlarkddnr1686/evertrip-image:latest
                            '''
                    }
                }
                }
            }
        }
        stage('Deploy to EC2') {
            steps {
            withCredentials([sshUserPrivateKey(credentialsId: 'ec2-ssh-key-id', keyFileVariable: 'SSH_KEY')]) {
                script {
                    sshagent(['ec2-ssh-key-id']) {
                                    sh '''
                                    #!/bin/bash
                                    ssh -i ${SSH_KEY} -o StrictHostKeyChecking=no ubuntu@${EC2_HOST} << EOF
                                    JASYPT_PASSWORD=${JASYPT_PASSWORD}
                                    docker pull rlarkddnr1686/evertrip-image:latest &&
                                    docker stop evertrip-container || true &&
                                    docker rm evertrip-container || true &&
                                    echo "Starting container with JASYPT_PASSWORD=\${JASYPT_PASSWORD}" &&
                                    docker run -d --network host -p 8080:8080 --name evertrip-container -e JASYPT_PASSWORD=\${JASYPT_PASSWORD} -e SPRING_PROFILES_ACTIVE=prod -e DISABLE_EC2_METADATA=true rlarkddnr1686/evertrip-image:latest
                                    '''
                                }
                    }
               }
            }
        }
    }

    post {
        success {
            echo 'Pipeline succeeded!'
        }
        failure {
            echo 'Pipeline failed!!'
        }
    }
}

위의 파이프라인 스크립트는 여러 단계를 거쳐 애플리케이션을 배포하게 됩니다. 각 스테이지는 아래와 같은 순서로 동작하게 됩니다.

1. Checkout

Git 저장소의 main 브랜치에서 코드를 체크아웃하고 가져오게 됩니다.

2. Set Permissions

gradlew 파일에 실행 권한을 부여합니다.

3. Build and Test

환경 변수와 함께 Gradle 명령을 사용하여 애플리케이션을 빌드하고 테스트합니다.

4. Build Docker Image

애플리케이션의 Docker 이미지를 빌드합니다. rlarkddnr1686/evertrip-image:latest 이미지를 빌드하게 됩니다.

5. Push Docker Image

위에서 생성한 DockerHub Credentials를 이용하여 Docker Hub에 로그인하고, Docker 이미지를 푸시합니다.

6. Deploy to EC2

위에서 생성한 SSH Credentials를 사용하여 배포하려는 EC2 인스턴스에 접속하고, 기존 컨테이너를 중지하고 제거한 후, 새로운 Docker 이미지를 다운받아 실행합니다. 환경 변수를 설정하여 컨테이너를 실행하게 됩니다.

7. Post Steps

파이프라인의 성공 유무에 따른 메시지를 출력하게 됩니다.

🎈 7. GitHub Repository Webhooks 설정

다음으로 GitHub Repository Webhooks를 설정해보도록 하겠습니다.

GitHub의 Repository -> Settings -> Webhooks -> Add webhook를 클릭하여 Webhook을 설정 해줍니다.

위의 사진과 같이 Payload URLhttp://Jenkins주소/github-webhook/를 기입하고 Content typeapplication/json을 선택해줍니다.

🎈 8. 테스트 해보기


이제 main 브랜치에 Pull Request를 올려 테스트를 해보도록 하겠습니다. Pull RequestWebhook -> RecentDeliveries를 확인해보니 Jenkins에게 코드 변경 사항에 대한 알림이 제대로 간 것을 확인할 수 있습니다.

이제 Jenkins로 가서 위에서 생성한 Pipeline이 제대로 동작하는지 확인해보도록 하겠습니다.

해당 빌드가 성공적으로 마친 것을 확인할 수 있습니다.

마지막으로 배포 서버의 주소로 API 명세를 확인할 수 있습니다.

☕ 마치며..


이번 포스팅에서는 Jenkins + Docker를 활용한 CI/CD 작업을 진행하였습니다. 해당 과정을 거치면서 로컬에서 작성한 개발 코드들의 배포 과정을 자동화하여 효율성을 높일 수 있었습니다.

CI/CD 파이프라인을 설정하는 데 다소 시간이 걸렸지만, 일단 완성되고 나니 자동화된 CI/CD가 수동 배포와 비교하여 훨씬 간편하고 효율적이라는 것을 직접 깨달을 수 있었습니다.

📌 참고

rungoat.log의 [CI/CD] Jenkins와 GitHub 연동하기 포스팅
[Jenkins] SSH 사용 - pipeline SSH Agent 포스팅
bagt13.log의 Jenkins와 Docker로 CI/CD pipeline 구축하기 포스팅

profile
TO BE DEVELOPER

0개의 댓글

관련 채용 정보