카테고리 없음

Jenkins로 Docker Container 배포하기

뽀글뽀글 개발자 2024. 8. 13. 10:24

젠킨스를 통해서 도커 컨테이너를 배포하는 pipeline script를 작성하기로 했다.

작성 과정에서 꽤 많은 설정이 필요했기 때문에 다음에 같은 문제를 겪지 않기 위해 기록한다.

 

이 글은 pipeline 생성이나 webhook 연동 같은 내용은 생략하고, 스크립트와 네트워크 설정만 다룬다. 

 

CICD 진행 순서

  1. Github Actions를 통해서 CI를 진행    참고 자료 
  2. main branch에 merge 된 경우 webhook 트리거를 통해 Jekins pipeline 실행
  3. 프로젝트를 clone하고 프로젝트에 필요한 secret 정보를 저장한 submodule 초기화
  4. .jar 파일 생성
  5. 도커 이미지 생성
  6. 생성된 이미지를 개인 도커 레지스트리에 push
  7. SSH로 배포 서버에 실행 중인 컨테이너를 지우고, 새로운 컨테이너 실행
  8. 배포 성공 시 다른 팀원들이 알 수 있게 Discord로 알림 (front 2, back 1로 진행 중이라 성공만 팀에게 알리면 됨)
  9. 배포 실패 시 내가 알 수 있게 email 알림

 

 

Git Clone & Submodule init

stage('Git Clone & Submodule Init') {
    steps {
        checkout scmGit(
            branches: [[name: '클론할 브랜치 이름']],
            extensions: [submodule(parentCredentials: true, reference: '', recursiveSubmodules: true, trackingSubmodules: true)],
            userRemoteConfigs: [[
                url: '깃허브 프로젝트 주소 -> 서브 모듈 주소 X',
                credentialsId: 'github access token으로 생성한 jenkins credentials'
            ]]
        )
    }
}
parentCredentials: true submodule을 초기화 할 때 상위인 배포 프로젝트의 credentials 사용
recursiveSubmodules: true submodule의 하위 submodule도 같이 가져옴
trackingSubmodules: true jenkins는 변경 사항이 있을 때만 스크립트를 실행하는데 submodule의 변경 사항이 있을 때도 체크함

 

 

 

.jar 파일 생성

stage('Gradle Build') {
    steps {
        script {
            sh './gradlew clean bootJar'
        }
    }
}

 CI에서 테스트를 진행했기 때문에 테스트나 기타 작업을 제외하고 Jar만 생성하는 bootJar 명령을 사용

 

 

Docker Image Build

Jenkins를 docker container로 사용하고 있는 경우 기본적으로는 jenkins 내에서 docker 명령을 사용할 수 없다.

docker.sock을 마운트하고 jenkins 컨테이너에서 docker.io를 설치해야한다.

.sock를 마운트하기 전에 jenkins를 실행하고 있는 사용자를 docker group에 추가해야한다.

# 젠킨스 실행 계정 이름 조회
docker inspect jenkins --format '{{.Config.User}}'

sudo groupadd docker
sudo usermod -aG docker ${젠킨스 실행 계정 이름}
newgrp docker
chmod 666 /var/run/docker.sock
ls -l /var/run/docker.sock

sudo docker run -d --name jenkins -v /home/rocky/docker/volume:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins

sudo docker exec -it --user root jenkins bash
apt-get update
apt-get install docker.io

 

도커 레지스트리를 http로 사용 중이라면 http로 통신하기 위한 설정을 추가해야한다.

주의 할 점은 설정을 받는 쪽과 보내는 쪽 모두 해줘야한다. 즉, jenkins 서버와 docker registry 서버 모두 해줘야한다. 

vi /etc/docker/daemon.json

# 아래 내용 추가
{
    "insecure-registries": ["125.247.92.91:5000"]
}

# 반영
sudo systemctl daemon-reload
sudo systemctl restart docker
environment {
    IMAGE_NAME = '이미지 이름'
    IMAGE_TAG = '태그'
    DOCKER_REGISTRY_URL = credentials('등록한 credentials의 ID')
}

stage('Docker Build') {
    steps {
        script {
            sh "docker build -t ${DOCKER_REGISTRY_URL}/${IMAGE_NAME}:${IMAGE_TAG} ."
        }
    }
}

 

 

Docker Push to Registry

docker registry credentials로 인증 정보를 전달하기 위해 registry에 인증 정보를 설정해서 실행해야한다.

# id, password 생성
sudo apt-get update
sudo apt-get install apache2-utils
htpasswd -B /home/pi/auth/htpasswd <another-username>

# 생성한 인증 정보 마운트
docker container run -d -p 5000:5000 --name registry --restart=always
-v /home/pi/registry:/var/lib/registry
-v /home/pi/auth:/auth
-e "REGISTRY_AUTH=htpasswd"
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm"
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd"
registry
stage('Docker Push') {
    steps {
        script {
            withCredentials([usernamePassword(credentialsId: 'docker-registry', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
                sh '''
                    echo $DOCKER_PASSWORD | docker login $DOCKER_REGISTRY_URL --username $DOCKER_USERNAME --password-stdin
                    docker push $DOCKER_REGISTRY_URL/$IMAGE_NAME:$IMAGE_TAG
                '''
            }
        }
    }
}

 

 

 

Container Deploy

jenkins container에서 ssh를 실행하려면 openssh를 설치해야한다.

sudo apt update
sudo apt install openssh-server
sudo systemctl status ssh

 

SSH Key 생성은 클라이언트에서 Key를 생성하고 pub 키를 서버에 authorized_keys에 저장하면 된다.

여기서는 젠킨스가 배포 서버에 접속하니까 젠킨스에서 키를 생성하고 배포 서버에 pub를 등록한다.

이후 jenkins credentials에 ssh키를 등록하면 된다. 

ssh-keygen -t rsa
cat ~/.ssh id_rsa.pub

# 복사 후 배포 서버의 authorized_keys 파일에 저장
vi ~/.ssh/authorized_keys
stage('Container Deploy') {
    steps {
        sshagent(credentials: ['젠킨스 ssh credentials']) {
            withCredentials([usernamePassword(credentialsId: '도커 레지스트리 credentials', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
                sh """
                    ssh -p ${SSH_PORT} -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_IP} '
                        echo ${DOCKER_PASSWORD} | sudo docker login ${DOCKER_REGISTRY_URL} --username ${DOCKER_USERNAME} --password-stdin
                        sudo docker stop ${CONTAINER_NAME} || true
                        sudo docker rm ${CONTAINER_NAME} || true
                        sudo docker pull ${DOCKER_REGISTRY_URL}/${IMAGE_NAME}:${IMAGE_TAG}
                        sudo docker run -d -p ${PORT_MAPPING} --restart always --name ${CONTAINER_NAME} ${DOCKER_REGISTRY_URL}/${IMAGE_NAME}:${IMAGE_TAG}
                    '
                """
            }
        }
    }
}

ssh로 sh를 실행할 때 주의할 점은 $변수명을 사용하면 sh에서 변수를 가져오는 것으로 sh를 실행하는 환경에 있는 변수를 가져온다.

${변수명}을 사용하면 jenkins pipeline에서 변수를 할당해서 완성된 script를 ssh에서 실행한다.

따라서 $변수명을 사용하면 빈 값이 들어가서 에러가 발생하기 때문에 ${변수명}을 사용해야한다.

 

 

 

알림 설정

post {
    success {
        script {
            withCredentials([string(credentialsId: 'discord-webhook', variable: 'DISCORD_WEBHOOK_URL')]) {
                def startTime = currentBuild.startTimeInMillis ? new Date(currentBuild.startTimeInMillis).format('yyyy-MM-dd HH:mm:ss') : '알 수 없음'
                def endTime = currentBuild.getTimeInMillis() ? new Date(currentBuild.getTimeInMillis()).format('yyyy-MM-dd HH:mm:ss') : '알 수 없음'

                discordSend(
                    description: """
                        **Study Service Monolithic CICD #${env.BUILD_NUMBER}**
                        **Status**: ${currentBuild.currentResult}
                        **프로젝트**: ${env.JOB_NAME}
                        **시작 시간**: ${startTime}
                        **종료 시간**: ${endTime}
                    """,
                    link: env.BUILD_URL,
                    result: 'SUCCESS',
                    title: "${env.JOB_NAME} #${env.BUILD_NUMBER}",
                    webhookURL: "${DISCORD_WEBHOOK_URL}"
                )
            }
        }
    }
    failure {
        script {
            def startTime = currentBuild.startTimeInMillis ? new Date(currentBuild.startTimeInMillis).format('yyyy-MM-dd HH:mm:ss') : '알 수 없음'
            def endTime = currentBuild.getTimeInMillis() ? new Date(currentBuild.getTimeInMillis()).format('yyyy-MM-dd HH:mm:ss') : '알 수 없음'
            def recipient = "${MY_EMAIL}"
            def subject = "Jenkins Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
            def body = """
            <h2>Jenkins Deployment Failed</h2>
            <ul>
                <li><strong>Job Name:</strong> ${env.JOB_NAME}</li>
                <li><strong>Build Number:</strong> ${env.BUILD_NUMBER}</li>
                <li><strong>Status:</strong> ${currentBuild.currentResult}</li>
                <li><strong>Start Time:</strong> ${startTime}</li>
                <li><strong>End Time:</strong> ${endTime}</li>
                <li><strong>Build URL:</strong> <a href="${env.BUILD_URL}">${env.BUILD_URL}</a></li>
            </ul>
            <p>Please check the Jenkins console output for more details.</p>
            """
            emailext(
                to: recipient,
                subject: subject,
                body: body,
                mimeType: 'text/html'
            )
        }
    }
}

 

 

 

처음 젠킨스 스크립트를 작성해본지라 작성된 내용이 잘 작성된 것이 아니라는 느낌이 든다.

일단은 실행은 잘 되니 더 필요한 것이 생기거나 다음에 또 스크립트를 작성하게 되면 더 깔끔하게 작성해보도록 하겠다.