Jenkins로 Docker Container 배포하기
젠킨스를 통해서 도커 컨테이너를 배포하는 pipeline script를 작성하기로 했다.
작성 과정에서 꽤 많은 설정이 필요했기 때문에 다음에 같은 문제를 겪지 않기 위해 기록한다.
이 글은 pipeline 생성이나 webhook 연동 같은 내용은 생략하고, 스크립트와 네트워크 설정만 다룬다.
CICD 진행 순서
- Github Actions를 통해서 CI를 진행 참고 자료
- main branch에 merge 된 경우 webhook 트리거를 통해 Jekins pipeline 실행
- 프로젝트를 clone하고 프로젝트에 필요한 secret 정보를 저장한 submodule 초기화
- .jar 파일 생성
- 도커 이미지 생성
- 생성된 이미지를 개인 도커 레지스트리에 push
- SSH로 배포 서버에 실행 중인 컨테이너를 지우고, 새로운 컨테이너 실행
- 배포 성공 시 다른 팀원들이 알 수 있게 Discord로 알림 (front 2, back 1로 진행 중이라 성공만 팀에게 알리면 됨)
- 배포 실패 시 내가 알 수 있게 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'
)
}
}
}
처음 젠킨스 스크립트를 작성해본지라 작성된 내용이 잘 작성된 것이 아니라는 느낌이 든다.
일단은 실행은 잘 되니 더 필요한 것이 생기거나 다음에 또 스크립트를 작성하게 되면 더 깔끔하게 작성해보도록 하겠다.