
이번 포스트는 지난 포스트에 특정 브랜치 (배포 브랜치)에 푸시가 되면 (Github Webhook 이용), Jenkins를 이용해 배포 자동화까지 이루어지는 과정에 대해 설명합니다.
Jenkins는 docker 이미지를 통해 설치를 진행합니다. (직접 서버에 설치해도 무관합니다)
# 도커 이미지 받기
sudo docker pull jenkins/jenkins:lts-jdk17
# 도커 컨테이너 실행 (이미지를 받지 않은 상태에서 실행하면 자동으로 이미지를 받아서 실행)
sudo docker run -d -p 8080:8080 -p 50000:50000 --restart=on-failure --name jenkins --privileged \
-v /jenkins:/var/jenkins_home \
-v /usr/bin/docker:/usr/bin/docker \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/jenkins:lts-jdk17
여기서 도커 관련 설정이 중요합니다.
jenkins에서 도커 이미지를 빌드하여 docker hub에 푸시하게 됩니다. 이 과정에서 jenkins에서 docker를 사용하기 위해서 host의 도커를 사용할 수 있도록 해주는 것 입니다.
이후에 로그를 설정된 초기 비밀번호를 알아냅니다.
sudo docker logs jenkins_server

해당 비밀번호는 초기 로그인을 위해 꼭!! 필요합니다.
현재 AWS에 서버를 설정한 경우, 네트워크 인바운드 규칙을 통해 8080번 (젠킨스 설정 포트) 포트를 열어줘야 합니다.
EC2 -> 보안 그룹 -> 인바운드 규칙 -> 인바운드 규칙 편집

각자 환경에 맞게 설정해 줍니다. (본인 IP 등등)

이후, 서버 IP or 도메인의 8080 포트에 접속해 여기에 이전에 받은 초기 비밀번호를 입력합니다.

추천 플러그인을 설치합니다. 필요한 플로그인은 설정 완료후에 추가적으로 설치할 수 있습니다.

설치가 완료되면, 관리자 계정 설정이 나옵니다. 사용할 계정을 입력하면 설치까지 완료됩니다.

배포를 위해 필요한 플러그인을 설치합니다.
이외에도 필요한 플러그인이 있다면 설치하면 됩니다.
Jenkins 사용을 위한 기본 설정입니다.
jenkins에는 Nodejs가 설치되어 있지 않기에, 설정을 해야 합니다.
Jenkins 관리 > Tools

자동으로 설치할 수 있도록, Install automatically를 체크해줍니다.
버전의 경우 프로젝트에서 사용하는 버전에 맞게 설정합니다.
사용법은 파이프라인에서 설명하도록 하겠습니다.
Jenkins에서 Github에 접근하기 위해서는 계정의 Access Token이 필요합니다.
Settings > Developer Settings > Personal access tokens > Tokens (classic) > Generate new token (classic)

Expiration의 경우, 각자 상황에 맞게 선택

접근 권한의 경우, 저장소와 repo_hook을 선택

생성을 완료하면 액세스 토큰을 발급해줍니다. 한 번만 알려주므로, 꼭 저장해야 합니다.
(잃어버리는 경우, 재발급)
Dashboard > Jenkins 관리 > Credentials > (global) 클릭 > Add Credentials

Jenkins를 Docker로 띄웠기 때문에, 서버 접속을 위해 필요합니다. Jenkins를 Docker로 띄우지 않았더라도, 분산 환경에서 배포하는 경우에도 서버 접속을 위해 필요합니다.
사전에 발급 받은 개인 키의 내용을 복사합니다. ex) 개인키.pem

Docker Hub 접속을 위한 설정입니다. 액세스 토큰이 필요합니다.
참고 : [(Nginx+React)+SpringBoot] 웹 서비스 배포 - Docker Hub 활용#Docker hub와 연결

테스트 Job을 생성하는 과정을 통해 Webhook 설정을 알아보겠습니다.
job 생성시에는 pipline을 선택합니다.


Github Webhook은 Gitlab과 달리 커스터마이징이 불가능합니다. (굉장히 불편)
특정 브랜치에서의 변화를 감지해서 Webhook을 보내지 못합니다. 모든 브랜치에서 정해진 변화가 발생하면 Webhook이 발생합니다.
이를 해결하기 위해 Generic Webhook Trigger라는 플러그인을 사용합니다.

웹훅에 의해 트리거되며 데이터를 json으로 받고, 해당 데이터 중에 branch를 사용하기 위해 변수를 설정합니다.

나중에 웹훅이 발생하면 payload에 push되는 브랜치를 ref라는 변수에 담아 전송합니다.
ref에 담긴 데이터를 내부에서 branch라는 변수로 사용한다는 의미입니다.

job을 구분하기 위한 토큰을 설정합니다.
기본적으로 http://JENKINS_URL/generic-webhook-trigger/invoke 로 입력을 받지만
토큰을 설정해주면 http://JENKINS_URL/generic-webhook-trigger/invoke?token=설정값 으로 입력받게 됩니다. (설정된 토큰이 전달되어야만 job이 실행)
위에서 설정했던 변수를 사용하기 위한 필터를 설정합니다. (변수와 정규 표현식을 이용)
정규 표현식 참고 - https://regexr.com/, https://regexper.com/

branch라는 변수가 refs/heads/test일 경우에만 job이 실행됩니다.
추가적으로 원하는 값이 있는 경우 설정하면 됩니다.


각 저장소의 Settings > Code and automation > Webhooks > Add webhook

Payload URL의 경우, 위에서 설정한 토큰 값을 쿼리 파라미터로 넘겨주면 됩니다
ex) token이 test인 경우, http://젠킨스URL/generic-webhook-trigger/invoke?token=test
Jenkins에서 json 형식으로 데이터를 받아서 처리하기에 Content type은 json으로 해 주어야 합니다.
trigger의 경우, 상황에 맞춰서 설정 (only push, pr 등등)
test 브랜치와 test2 브랜치를 생성해서 각각의 브랜치로 job이 트리거되는지 확인해 보겠습니다.

test2 브랜치에 push가 발생해서 test 토큰으로 POST 요청을 보냈습니다.

job과 정규 표현식 필터, 트리거 여부를 응답 받습니다. 정규 표현식 필터에 의해 트리거되지 않았습니다.


trigger가 되었다는 응답을 받았습니다.

데이터의 처리와 출력이 단계별로 이어지는 구조를 말합니다.
배포에서의 파이프라인은 일반적으로 코드 빌드, 테스트, 배포의 구조를 가집니다.
각 파이프라인에서 stage라는 개념이 등장합니다. 각 단계를 구분하기 위해 사용합니다.
(빌드, 테스트, 배포)
프론트엔드, 백엔드 모두에서 사용하는 파이프라인입니다.
environment {
dockerNetwork = ''
dockerContainer = ''
registryCredential = ''
dockerImage = ''
releaseServerAccount = ''
releaseServerUri = ''
releasePort = "${env.FRONTEND_PORT}"
}
파이프라인에서 사용 할 환경 변수를 정의합니다. Jenkins Global 환경 변수 삽입도 가능합니다.
stage('Git Clone') {
steps {
git branch: '사용할 브랜치',
credentialsId: 'GITHUB 자격 증명 ID',
url: 'GITHUB URL'
}
}
Github에서 코드를 받아오기 위한 설정입니다.
프론트엔드의 경우, [(Nginx+React)+SpringBoot] 웹 서비스 배포 - Docker Hub 활용#dockerfile-만들기에서의 Dockerfile을 사용합니다.
프론트엔드의 경우, NodeJS를 사용하기 위한 설정이 필요합니다. 이전에 설정했던 NodeJS를 불러와서 사용합니다.
stage('Install Node.js and npm') {
steps {
script {
def nodejsHome = tool name: 'NodeJS', type: 'jenkins.plugins.nodejs.tools.NodeJSInstallation'
env.PATH = "${nodejsHome}/bin:${env.PATH}"
}
}
}
배포를 위한 빌드과정입니다.
stage('[FRONTEND] Node Build') {
steps {
sh 'npm install'
sh 'npm run build'
}
}
stage('[FRONTEND] Build & DockerHub Push') {
steps {
script {
docker.withRegistry('https://registry.hub.docker.com', registryCredential) {
sh 'docker buildx build -t ${dockerImage} --push .'
}
}
}
}
2024.08.01 기준, 기존 docker pipeline 플러그인에서 지원하는
docker.build()를 사용할 수 없습니다. (해당 플러그인에서 buildx 미지원)
따라서 shell 명령어를 통해 직접 위와 같은 형태로 지정해야 합니다.
AWS 서버에 접속하기 위해, SSH Agent 플러그인을 이용합니다.
새로운 서비스와의 충돌을 방지하기 위해, 기존 서비스는 중지하는 과정입니다.
stage('[FRONTEND] Before Service Stop') {
steps {
sshagent(credentials: ['SSH 자격 증명']) {
sh '''
if test "`ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "docker ps -aq --filter name=${dockerContainer}"`"; then
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "docker container stop $(docker ps -aq --filter name=${dockerContainer})"
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "docker container rm -f $(docker ps -aq --filter name=${dockerContainer})"
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "docker image rmi ${dockerImage}"
fi
'''
}
}
}
만약 Docker Container가 존재한다다면 실행을 멈추고, 삭제합니다. 이미지도 사용할 일이 없기에 삭제합니다.
Docker Hub에 올려진 이미지를 사용하기 위해 받아오는 과정입니다.
stage('[FRONTEND] DockerHub Pull') {
steps {
sshagent(credentials: ['SSH 자격 증명]) {
sh '''
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} <<EOF
echo "${DOCKER_HUB_ACCESS_TOKEN}" | docker login -u "username" --password-stdin
docker pull ${dockerImage}
docker logout
EOF
'''
}
}
}
서비스 사용을 위해 프론트엔드를 띄우는 과정입니다.
stage('[FRONTEND] Service Start') {
steps {
sshagent(credentials: ['SSH 자격 증명']) {
sh '''
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "sudo docker run -i -e TZ=Asia/Seoul -v /home/ubuntu/backup/frontend:/backup/frontend --name ${dockerContainer} --network ${dockerNetwork} -p ${releasePort}:80 -d ${dockerImage}"
'''
}
}
}
프론트엔드와 큰 틀은 거의 비슷합니다.
MSA로 백엔드 환경을 구축하는 경우, 서비스 별로 Job과 파이프라인을 구축하여 배포하면 독립성을 유지할 수 있습니다.
아래는 여러 서비스 중, 유레카 서비스를 배포하는 과정입니다. 다른 서비스를 배포하는 경우 여기서 달라지는 것이 크게 없습니다. (테스트 과정 정도)
FROM --platform=linux/amd64 openjdk:17-alpine
# jar 설정
ARG JAR_FILE=/build/libs/eureka-service-1.0.jar
# jar 복사
COPY ${JAR_FILE} /EurekaService.jar
# PORT
EXPOSE 8761
# Set the entry point for the container to run the Spring Boot application
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/EurekaService.jar"]
stage('[Eureka] Spring Build') {
steps {
dir ('backend/eureka-service') {
sh 'chmod +x ./gradlew'
sh './gradlew clean bootJar'
}
}
}
stage('[Eureka] Build & DockerHub Push') {
steps {
dir('backend/eureka-service') {
script {
docker.withRegistry('https://registry.hub.docker.com', registryCredential) {
sh 'docker buildx build -t ${dockerImage} --push .'
}
}
}
}
}
stage('[Eureka] Before Service Stop') {
steps {
sshagent(credentials: ['SSH 자격 증명']) {
sh '''
if test "`ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "docker ps -aq --filter name=${dockerContainer}"`"; then
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "docker container stop $(docker ps -aq --filter name=${dockerContainer})"
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "docker container rm -f $(docker ps -aq --filter name=${dockerContainer})"
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "docker image rmi ${dockerImage}"
fi
'''
}
}
}
stage('[Eureka] DockerHub Pull') {
steps {
sshagent(credentials: ['SSH 자격 증명']) {
sh "ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} 'sudo docker pull ${dockerImage}'"
}
}
}
stage('[Eureka] Service Start') {
steps {
sshagent(credentials: ['SSH 자격 증명']) {
sh '''
ssh -o StrictHostKeyChecking=no ${releaseServerAccount}@${releaseServerUri} "sudo docker run -d -e TZ=Asia/Seoul -p 8761:8761 --network ${dockerNetwork} -e "spring.profiles.active=${springProfiles}" --name ${dockerContainer} ${dockerImage}"
'''
}
}
}
흔히들 서버 배포라는 말을 듣게 되면 지레 겁을 먹는 경우가 많습니다.
하지만 서버 배포라는 과정은 빌드하는 장소가 로컬에서 서버로 바뀌는 것 뿐 나머지는 평상시와 거의 동일합니다. 그래서 한 두번 이러한 과정을 겪게 되면 쉽게 누구나 따라할 수 있으리라 생각합니다.
또한 배포하는 과정에서 프론트엔드와 백엔드엔드를 서로 이해하여 배포하는 것이 중요하기 때문에 프론트엔드와 백엔드의 직무를 구분하지 않고 개발자라면 서비스를 배포해보는 경험을 겪는 것이 개발에서 큰 도움이 된다고 생각합니다.