서론
- 지난 포스트에서는 Jenkins 설치 및 설정, GitHub 토큰 생성 과정을 다루었습니다.
- 이번 글에서는 Jenkins를 활용한 파이프라인 생성부터 배포까지의 과정을 상세히 살펴보겠습니다.
1. 새로운 파이프라인 생성
- 파이프라인을 생성하고 이름을 설정합니다. 아래의 이미지를 참고하여 단계별로 설정을 진행하세요.
2. 환경변수 파일 로드
1) 환경변수 파일 생성
- 환경변수를 저장할 파일을 생성합니다.
- 파일명은
env.properties
로 하고, 아래 예시와 같이 key-value 형태로 작성합니다.
# AWS 설정
AWS_REGION=ap-northeast-2
ASG_NAME=net-novel-backend-ASG
2) 파이프라인에서 환경변수를 읽는 Stage 생성
- 아래 코드를 사용하여 환경변수를 로드하고 전역변수(
env.
형태)로 설정합니다.
//환경변수들을 로드
stage('Load Env Variables') {
steps {
script {
// env.properties 파일 읽기
def filePath = '/home/ubuntu/properties/env.properties'
def fileContent = readFile(filePath)
// 파일 내용을 key-value로 변환
def props = [:]
fileContent.split('\n').each { line ->
line = line.trim()
if (line && !line.startsWith("#")) { // 빈 줄과 주석 제외
def keyValue = line.split('=')
if (keyValue.size() == 2) {
props[keyValue[0].trim()] = keyValue[1].trim()
}
}
}
// 반복문을 돌며환경 변수 설정
props.each { key, value ->
env."${key}" = value
}
//출력테스트
echo "SPRING_PROPS_FILE: ${env.SPRING_PROPS_FILE}"
}
}
}
3. GitHub 클론 파이프라인 생성
- 연결된 GitHub Repository의 소스코드를 clone 합니다.
- Public 레포지토리 기준으로 코드를 작성하였습니다.
//github 소스코드 Clone, jenkins workspace에 저장
stage('Clone Repository') {
steps {
echo 'Cloning GitHub repository...'
git branch: 'master', url: "${env.REPO_URL}"
}
}
- 클론된 레포지토리는 Jenkins 작업공간(Workspace)에서 확인 가능합니다.
- 또는 ec2 인스턴스 내에서 jenkins workspace로 이동하여 확인할 수 있씁니다.
cd /var/lib/jenkins/workspace/<pipeline name> && ls
4. Gradle로 Spring Application Build
- build 하기전에 먼저 RAM 용량이 부족하여, build에 실패할수있으니, 아래글을 참고하여 메모리스왑을 진행하시기바랍니다.
- 메모리스왑관련글은 아래를 참고해주세요!
- 2024.12.16 - [AWS/EC2] - [EC2] Ubuntu Swap Memory 설정으로 메모리 부족 문제 해결(EC2 멈춤 해결)
[EC2] Ubuntu Swap Memory 설정으로 메모리 부족 문제 해결(EC2 멈춤 해결)
1. 서론EC2에서 Gradle 빌드 시 인스턴스가 멈추는 현상이 발생했습니다.이는 메모리 부족으로 인해 발생한 문제로, 스왑 메모리(swap)를 설정하여 해결할 수 있었습니다.이번 글에서는 스왑 메모리
myrail.tistory.com
1) application.properteis
복사
- GitHub에는
application.properties
파일을 푸시하지 않으므로, Jenkins에서 해당 파일을 전달합니다.
//Spring JAR 파일 만들기위한 application.properties 복사
stage('Copy application.properties') {
steps {
echo 'Copy application.properties to target directory'
sh "cp ${env.SPRING_PROPS_FILE} ${env.TARGET_DIR}"
}
2) gradlew
실행권한 추가
- 파이프라인 실행 중,
gradlew
권한 문제로 실행이 되지않았습니다. gradlew
에 실행권한x
를 추가하여 문제를 해결하였습니다.
stage('Ensure gradlew') {
steps {
echo 'Ensure gradlew has execute permission'
sh 'chmod +x ./gradlew'
}
}
3) JAR 파일 빌드
- 로컬에서 테스트를 진행했다고 가정하고, 불필요한 test를 스킵했습니다.
//JAR 파일 build
stage('Build Spring JAR') {
steps {
echo 'Build Spring JAR'
sh './gradlew clean build -x test'
}
}
5. Docker Image Build 및 Docker Hub Push
- 먼저 인스턴스에 Docker 설치가 필요합니다. 설치방법은 도커 공식문서 를 참고해주세요
1) Jenkins Docker 권한부여
- Jenkins 사용자를 Docker 그룹에 추가합니다.
- 권한부여후 적용이 되지않는경우, Jenkins를 재부팅해줍니다.
# docker group에 젠킨스 넣기
sudo usermod -aG docker jenkins
# Jenkins 재부팅
sudo systemctl restart jenkins
2) Docker image build
- 앞서 만든 Spring
jar
파일 기준으로Docker Image
을 만듭니다. - GitHub에서 받은 소스코드 안에
Dockerfile
이 작성되어 있어야 합니다
stage("Build Docker Image") {
steps {
echo 'Build Docker Image'
sh "docker build -t ${env.DOCKER_IMAGE_NAME}:${env.DOCKER_IMAGE_TAG} ."
}
}
- 빌드가 완료되면 아래 명령어로 생성된 이미지를 확인할 수 있습니다.
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
image/backend latest df4cf5d8614e 21 hours ago 417MB
3) Dockr-Compose Up 테스트
- Docker Hub에 이미지를 푸시하기 전에 정상적으로 실행되는지 확인합니다.
- 실행 후 5분 뒤 API를 호출하여 Health Check를 수행합니다.
- 헬스체크 실패시 다음과같이 에러를 출력합니다.
//Docker Compose 배포테스트
//헬스체크 API 호출, 실패시 에러로처리
stage("Docker Compose Deployment and Health Check") {
steps {
echo "Docker Compose Deployment and Health Check..."
//Docker compose 실행
sh """
cd ${env.DOCKER_COMPOSE_YML}
docker-compose up -d
"""
sleep(300) // 컨테이너 실행 대기시간, 초단위
//컨테이너의 헬스체크 API 호출, 성공시 200 반환
//200이 아닐경우 error로 파이프라인 종료
script {
def httpResponse = sh(
script: """
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8081/api/health || true
""",
returnStdout: true
).trim()
//응답받은 HTTP 코드 로그 출력
echo "HTTP Response: ${httpResponse}"
//HTTP 200코드가 아닐경우 로그 출력후 에러처리
if (httpResponse != "200") {
echo "Backend Docker-Container Health check failed. Please check."
error("Health check failed: Received HTTP $httpResponse instead of 200.")
}
//HTTP 200코드인경우 성공 로그 출력
echo "Backend Docker-Container Health check passed successfully!!"
}
//Docker 컨테이너 종료
echo "Shutting down Docker container..."
sh """
cd ${env.DOCKER_COMPOSE_YML}
docker-compose down
"""
}
}
4) Docker hub push
- 생성한 Docker Image 파일을 EC2 인스턴스에서 사용할 수 있게 Docker Hub에 업로드합니다.
- Docker Hub에 이미지를 푸시하기 위해 Jenkins에 Docker 관련
Credentials
가 등록되어 있어야 합니다.
//Docker Hub에 push
stage("Push Docker Image") {
steps {
echo "Push Docker Image to Docker Hub"
withCredentials([usernamePassword(credentialsId:
'docker-hub-credentials',//젠킨스에 저장된 credentials 이름
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS')]) {
//도커 로그인후 이미지 push
// 도커 로그인
sh "docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}"
// 이미지 Push
sh "docker push ${env.DOCKER_IMAGE_NAME}:${env.DOCKER_IMAGE_TAG}"
}
}
}
- Jenkins 로그에서 이미지가 정상적으로 푸시된 것을 확인할 수 있습니다.
- Docker Hub에 접속하여 이미지가 업로드되었는지 확인합니다.
}
6. EC2 AutoScaling Group 인스턴스 재시작
- ASG(Auto Scaling Group)의 모든 인스턴스를 차례로 종료하고, 새 인스턴스를 생성합니다
- 새인스턴스는 부팅시 Docker 이미지를 Pulling 하여 최신 이미지를 적용합니다.
1) ASG의 인스턴스 목록 가져오기
- 실행 중인 ASG의 인스턴스 ID를 가져옵니다.
// Auto Scaling Group의 인스턴스 정보를 가져오는 스테이지
stage("Fetch Auto Scaling Group Instances") {
steps {
script {
//Auto Scaling Group에서 실행중인 인스턴스 ID를 받아옴
def asgInstances = sh(script: """
aws autoscaling describe-auto-scaling-groups \
--region ${env.AWS_REGION} \
--auto-scaling-group-names ${env.ASG_NAME} \
--query "AutoScalingGroups[0].Instances[?LifecycleState=='InService'].InstanceId" \\
--output text
""", returnStdout: true).trim()
// 줄바꿈을 기준으로 인스턴스 ID를 나눈 후 배열에 저장
env.ASG_INSTANCE_IDS = asgInstances
// Instance ID 로그에 출력
echo "ASG Instances: ${env.ASG_INSTANCE_IDS}"
}
}
}
2) ASG 인스턴스 종료 및 갱신
- 앞 가정에서 가져온 인스턴스 목록을 하나씩 종료합니다.
- 새 인스턴스가 생성될 때까지 대기하고, 생성이 완료되면 다음 인스턴스를 종료합니다.
- 목록의 모든 인스턴스가 종료될때까지 반복합니다.
// Auto Scaling Group의 인스턴스를 차례로 종료하는 스테이지
stage("Terminate and Refresh ASG Instances") {
steps {
script {
// 인스턴스 ID 목록 배열로 변환
def instanceList = env.ASG_INSTANCE_IDS.split("\\s+")
//ASG 인스턴스 ID 목록을 출력(디버깅용)
echo "Instance List: ${instanceList}"
// ASG의 초기 인스턴스 수를 저장
def initialInstanceCount = instanceList.size()
echo "Initial Instance Count: ${initialInstanceCount}"
// 각 인스턴스를 종료
instanceList.each { instanceId ->
//터미네이팅 시킬 인스턴스 이름 출력
echo "Terminating instance: ${instanceId}"
// 종료 명령 실행
sh """
aws autoscaling terminate-instance-in-auto-scaling-group \
--region ${env.AWS_REGION} \
--instance-id ${instanceId} \
--no-should-decrement-desired-capacity
"""
// 새 인스턴스가 준비될 때까지 대기
echo "Waiting for ASG to refresh all instances to InService state..."
// 최대 대기 시간(초) 설정
def maxRetries = 30 // 최대 반복 횟수 (30회 반복, 총 10분 대기)
def retryInterval = 20 // 각 반복 간 대기 시간 (초)
def retries = 0//반복횟수 카운트용 객체
//ASG의 인스턴스 수를 확인
//새로운 인스턴스가 생성되어 In Service 인스턴스 수가, Stage 시작전과 동일해질때까지 확인 반복
//반복 횟수가 초과될경우 에러 출력
while (true) {
echo " Retries = ${retries}"
def inServiceCount = sh(script: """
aws autoscaling describe-auto-scaling-groups \
--region ${env.AWS_REGION} \
--auto-scaling-group-names ${env.ASG_NAME} \
--query "AutoScalingGroups[0].Instances[?LifecycleState=='InService'] | length(@)" \
--output text
""",
returnStdout: true
).trim().toInteger()
echo "Current InService instance count: ${inServiceCount}"
if (inServiceCount == initialInstanceCount) {
echo "All instances are now in service!"
break
}
// 반복 횟수 증가 및 제한 확인
//최대 반복횟수에 도달하면 에러 출력
retries++
if (retries >= maxRetries) {
error("Max retries reached. Not all instances reached InService state within the expected time.")
}
echo "Waiting for instances to reach InService state..."
sleep(retryInterval)//다음 반복까지 잠시대기
}
}
}
// 모든 인스턴스 종료 완료 로그
echo "All specified instances have been terminated and refreshed successfully."
}
}
- ASG의 인스턴스 관리 또는, 활동->작업기록 에서 결과를 확인할 수 있습니다.
7. Slack에 알람보내기
- Jenkins 파이프라인 단계의 결과를 Slack으로 알림 보냅니다.
- Slack과 Jenkins 연동은 아래글을 참고해주세요!
2024.12.19 - [Jenkins] - [Jenkins] Jenkins 빌드 상태를 Slack으로 자동 알림 보내기
post {
always {
echo 'Pipeline compoleted!'
slackSend channel: '#netnovel', message: """
📢 *Pipeline 완료*
- *Job*: ${env.JOB_NAME}
- *Build Number*: ${env.BUILD_NUMBER}
- *결과*: ${currentBuild.currentResult}
- *빌드 링크*: ${env.BUILD_URL}
- *Duration*: ${currentBuild.durationString}
- *Commit*: ${env.GIT_COMMIT}
"""
}
success {
slackSend channel: '#netnovel', message: """
✅ *빌드 및 배포 성공!*
- *Job*: ${env.JOB_NAME}
- *Build Number*: ${env.BUILD_NUMBER}
- *빌드 링크*: ${env.BUILD_URL}
"""
}
failure {
slackSend channel: '#netnovel', message: """
❌ *빌드 실패!*
- *Job*: ${env.JOB_NAME}
- *Build Number*: ${env.BUILD_NUMBER}
- *빌드 링크*: ${env.BUILD_URL}
- *확인 후 문제 해결 필요!*
"""
}
}
- Slack에 다음과 같이 메시지가 전달됩니다.
8.파이프라인 전체코드
pipeline {
agent any
stages {
//환경변수들을 로드
stage('Load Env Variables') {
steps {
script {
// env.properties 파일 읽기
def filePath = '/home/ubuntu/properties/env.properties'
def fileContent = readFile(filePath)
// 파일 내용을 key-value로 변환
def props = [:]
fileContent.split('\n').each { line ->
line = line.trim()
if (line && !line.startsWith("#")) { // 빈 줄과 주석 제외
def keyValue = line.split('=')
if (keyValue.size() == 2) {
props[keyValue[0].trim()] = keyValue[1].trim()
}
}
}
// 반복문을 돌며환경 변수 설정
props.each { key, value ->
env."${key}" = value
}
//출력테스트
echo "SPRING_PROPS_FILE: ${env.SPRING_PROPS_FILE}"
}
}
}
//github 소스코드 Clone, jenkins workspace에 저장
stage('Clone Repository') {
steps {
echo 'Cloning GitHub repository...'
git branch: 'master', url: "${env.REPO_URL}"
}
}
//Spring JAR 파일 만들기위한 application.properties 복사
stage('Copy application.properties') {
steps {
echo 'Copy application.properties to target directory'
sh "cp ${env.SPRING_PROPS_FILE} ${env.TARGET_DIR}"
}
}
//gradlew 쓰기 권한 추가
stage('Ensure gradlew') {
steps {
echo 'Ensure gradlew has execute permission'
sh 'chmod +x ./gradlew'
}
}
//JAR 파일 build
stage('Build Spring JAR') {
steps {
echo 'Build Spring JAR'
sh './gradlew clean build -x test'
}
}
//Docker Image Build
stage("Build Docker Image") {
steps {
echo 'Build Docker Image'
sh "docker build -t ${env.DOCKER_IMAGE_NAME}:${env.DOCKER_IMAGE_TAG} ."
}
}
//Docker Compose 배포테스트
//헬스체크 API 호출, 실패시 에러로처리
stage("Docker Compose Deployment and Health Check") {
steps {
echo "Docker Compose Deployment and Health Check..."
//Docker compose 실행
sh """
cd ${env.DOCKER_COMPOSE_YML}
docker-compose up -d
"""
sleep(300) // 컨테이너 실행 대기시간, 초단위
//컨테이너의 헬스체크 API 호출, 성공시 200 반환
//200이 아닐경우 error로 파이프라인 종료
script {
def httpResponse = sh(
script: """
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8081/api/health || true
""",
returnStdout: true
).trim()
//응답받은 HTTP 코드 로그 출력
echo "HTTP Response: ${httpResponse}"
//HTTP 200코드가 아닐경우 로그 출력후 에러처리
if (httpResponse != "200") {
echo "Backend Docker-Container Health check failed. Please check."
error("Health check failed: Received HTTP $httpResponse instead of 200.")
}
//HTTP 200코드인경우 성공 로그 출력
echo "Backend Docker-Container Health check passed successfully!!"
}
//Docker 컨테이너 종료
echo "Shutting down Docker container..."
sh """
cd ${env.DOCKER_COMPOSE_YML}
docker-compose down
"""
}
}
//Dockr Hub에 push
stage("Push Docker Image") {
steps {
echo "Push Docker Image to Docker Hub"
withCredentials([usernamePassword(credentialsId:
'docker-hub-credentials',//젠킨스에 저장된 credentials 이름
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS')]) {
//도커 로그인후 이미지 push
// 도커 로그인
sh "docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}"
// 이미지 Push
sh "docker push ${env.DOCKER_IMAGE_NAME}:${env.DOCKER_IMAGE_TAG}"
}
}
}
// Auto Scaling Group의 인스턴스 정보를 가져오는 스테이지
stage("Fetch Auto Scaling Group Instances") {
steps {
script {
//Auto Scaling Group에서 실행중인 인스턴스 ID를 받아옴
def asgInstances = sh(script: """
aws autoscaling describe-auto-scaling-groups \
--region ${env.AWS_REGION} \
--auto-scaling-group-names ${env.ASG_NAME} \
--query "AutoScalingGroups[0].Instances[?LifecycleState=='InService'].InstanceId" \\
--output text
""", returnStdout: true).trim()
// 줄바꿈을 기준으로 인스턴스 ID를 나눈 후 배열에 저장
env.ASG_INSTANCE_IDS = asgInstances
// Instance ID 로그에 출력
echo "ASG Instances: ${env.ASG_INSTANCE_IDS}"
}
}
}
// Auto Scaling Group의 인스턴스를 차례로 종료하는 스테이지
stage("Terminate and Refresh ASG Instances") {
steps {
script {
// 인스턴스 ID 목록 배열로 변환
def instanceList = env.ASG_INSTANCE_IDS.split("\\s+")
//ASG 인스턴스 ID 목록을 출력(디버깅용)
echo "Instance List: ${instanceList}"
// ASG의 초기 인스턴스 수를 저장
def initialInstanceCount = instanceList.size()
echo "Initial Instance Count: ${initialInstanceCount}"
// 각 인스턴스를 종료
instanceList.each { instanceId ->
//터미네이팅 시킬 인스턴스 이름 출력
echo "Terminating instance: ${instanceId}"
// 종료 명령 실행
sh """
aws autoscaling terminate-instance-in-auto-scaling-group \
--region ${env.AWS_REGION} \
--instance-id ${instanceId} \
--no-should-decrement-desired-capacity
"""
// 새 인스턴스가 준비될 때까지 대기
echo "Waiting for ASG to refresh all instances to InService state..."
// 최대 대기 시간(초) 설정
def maxRetries = 30 // 최대 반복 횟수 (30회 반복, 총 10분 대기)
def retryInterval = 20 // 각 반복 간 대기 시간 (초)
def retries = 0//반복횟수 카운트용 객체
//ASG의 인스턴스 수를 확인
//새로운 인스턴스가 생성되어 In Service 인스턴스 수가, Stage 시작전과 동일해질때까지 확인 반복
//반복 횟수가 초과될경우 에러 출력
while (true) {
echo " Retries = ${retries}"
def inServiceCount = sh(script: """
aws autoscaling describe-auto-scaling-groups \
--region ${env.AWS_REGION} \
--auto-scaling-group-names ${env.ASG_NAME} \
--query "AutoScalingGroups[0].Instances[?LifecycleState=='InService'] | length(@)" \
--output text
""",
returnStdout: true
).trim().toInteger()
echo "Current InService instance count: ${inServiceCount}"
if (inServiceCount == initialInstanceCount) {
echo "All instances are now in service!"
break
}
// 반복 횟수 증가 및 제한 확인
//최대 반복횟수에 도달하면 에러 출력
retries++
if (retries >= maxRetries) {
error("Max retries reached. Not all instances reached InService state within the expected time.")
}
echo "Waiting for instances to reach InService state..."
sleep(retryInterval)//다음 반복까지 잠시대기
}
}
}
// 모든 인스턴스 종료 완료 로그
echo "All specified instances have been terminated and refreshed successfully."
}
}
}
post {
always {
echo 'Pipeline compoleted!'
slackSend channel: '#netnovel', message: """
📢 *Pipeline 완료*
- *Job*: ${env.JOB_NAME}
- *Build Number*: ${env.BUILD_NUMBER}
- *결과*: ${currentBuild.currentResult}
- *빌드 링크*: ${env.BUILD_URL}
- *Duration*: ${currentBuild.durationString}
"""
}
success {
slackSend channel: '#netnovel', message: """
✅ *빌드 및 배포 성공!*
- *Job*: ${env.JOB_NAME}
- *Build Number*: ${env.BUILD_NUMBER}
- *빌드 링크*: ${env.BUILD_URL}
"""
}
failure {
slackSend channel: '#netnovel', message: """
❌ *빌드 실패!*
- *Job*: ${env.JOB_NAME}
- *Build Number*: ${env.BUILD_NUMBER}
- *빌드 링크*: ${env.BUILD_URL}
- *확인 후 문제 해결 필요!*
"""
}
}
}
9. 결론
- 이번 포스트에서는 Jenkins를 활용한 CI/CD 파이프라인을 구축하며 GitHub 클론, Gradle 빌드, Docker 이미지 생성 및 배포 테스트, ASG를 통한 무중단 배포, Slack 알림 설정까지 진행했습니다.
- 이로써 배포 프로세스를 자동화하고, 안정성과 효율성을 높이는 데 성공했습니다.