과거에는 어찌되었는지 모르지만 github, gitlab, bitbucket(잘 모르겠음) 에서 CI 를 지원해주고 오픈소스가 많은 현대 소프트웨어 개발에서 CI/CD는 선택이 아니라 필수인 것 같다. 보안 문제가 아닌 이상 이 간편한걸 사용하지 않으면 바보라고 생각이 든다. 이번에는 Jenkins 를 이용해서 Spring Boot 프로젝트를 자동화된 빌드, 테스트, 배포 파이프라인을 구축을 시도해보았다.
CI/CD가 필요한 이유
- 빠른 피드백: 코드 변경 후 즉시 빌드/테스트 결과 확인
- 품질 보증: 자동화된 테스트로 버그 조기 발견
- 배포 자동화: 수동 배포의 실수 방지
- 개발 생산성: 반복 작업 자동화로 개발에 집중
Jenkins 설치 및 설정
Docker를 활용한 Jenkins 설치
docker run -d \
--name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
jenkins/jenkins:lts
초기 관리자 비밀번호는 다음 명령으로 확인 가능하다.
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
필수 플러그인 설치
Jenkins 대시보드에서 다음 플러그인을 설치합니다:
- Git Plugin: Git 저장소 연동
- Maven Integration: Maven 빌드 지원
- Pipeline: Jenkinsfile 기반 파이프라인
- Docker Pipeline: Docker 이미지 빌드/배포
- Blue Ocean: 시각적 파이프라인 UI
Spring Boot 프로젝트 준비
기본 프로젝트 구조
spring-boot-app/
├── src/
│ ├── main/java/
│ └── test/java/
├── pom.xml
├── Dockerfile
└── Jenkinsfile
Dockerfile 작성
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
application.yml 프로파일 설정
spring:
profiles:
active: ${SPRING_PROFILE:dev}
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:testdb
---
spring:
config:
activate:
on-profile: prod
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
Jenkinsfile 작성
선언형 파이프라인 예제
pipeline {
agent any
tools {
maven 'Maven-3.9'
jdk 'JDK-17'
}
environment {
DOCKER_IMAGE = 'myapp/spring-boot-app'
DOCKER_TAG = "${BUILD_NUMBER}"
DOCKER_REGISTRY = 'docker.io'
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'https://github.com/username/spring-boot-app.git'
}
}
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
jacoco(
execPattern: '**/target/jacoco.exec',
classPattern: '**/target/classes',
sourcePattern: '**/src/main/java'
)
}
}
}
stage('Code Analysis') {
steps {
script {
withSonarQubeEnv('SonarQube') {
sh 'mvn sonar:sonar'
}
}
}
}
stage('Package') {
steps {
sh 'mvn package -DskipTests'
}
}
stage('Docker Build') {
steps {
script {
docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
docker.build("${DOCKER_IMAGE}:latest")
}
}
}
stage('Docker Push') {
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
docker.image("${DOCKER_IMAGE}:${DOCKER_TAG}").push()
docker.image("${DOCKER_IMAGE}:latest").push()
}
}
}
}
stage('Deploy to Dev') {
steps {
sh '''
docker stop spring-boot-app-dev || true
docker rm spring-boot-app-dev || true
docker run -d \
--name spring-boot-app-dev \
-p 8081:8080 \
-e SPRING_PROFILE=dev \
${DOCKER_IMAGE}:${DOCKER_TAG}
'''
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
input {
message "배포를 진행하시겠습니까?"
ok "배포"
}
steps {
sh '''
docker stop spring-boot-app-prod || true
docker rm spring-boot-app-prod || true
docker run -d \
--name spring-boot-app-prod \
-p 8080:8080 \
-e SPRING_PROFILE=prod \
-e DB_URL=${PROD_DB_URL} \
-e DB_USERNAME=${PROD_DB_USERNAME} \
-e DB_PASSWORD=${PROD_DB_PASSWORD} \
${DOCKER_IMAGE}:${DOCKER_TAG}
'''
}
}
}
post {
success {
echo '파이프라인 성공!'
// Slack 알림 등 추가 가능
}
failure {
echo '파이프라인 실패!'
// 이메일 알림 등 추가 가능
}
always {
cleanWs()
}
}
}
고급 기능 구현
1. 멀티 브랜치 파이프라인
pipeline {
agent any
stages {
stage('Branch Strategy') {
steps {
script {
if (env.BRANCH_NAME == 'main') {
echo 'Production 배포 준비'
} else if (env.BRANCH_NAME.startsWith('develop')) {
echo 'Development 환경 배포'
} else if (env.BRANCH_NAME.startsWith('feature/')) {
echo 'Feature 브랜치 테스트만 수행'
}
}
}
}
}
}
2. 병렬 실행
stage('Parallel Tests') {
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test -Dtest=*UnitTest'
}
}
stage('Integration Tests') {
steps {
sh 'mvn test -Dtest=*IntegrationTest'
}
}
stage('Security Scan') {
steps {
sh 'dependency-check.sh --scan .'
}
}
}
}
3. Blue-Green 배포
stage('Blue-Green Deploy') {
steps {
script {
def currentColor = sh(
script: "docker ps --filter 'name=app-blue' --format '{{.Names}}'",
returnStdout: true
).trim() ? 'blue' : 'green'
def newColor = currentColor == 'blue' ? 'green' : 'blue'
sh """
docker run -d \
--name app-${newColor} \
-p 808${newColor == 'blue' ? '1' : '2'}:8080 \
${DOCKER_IMAGE}:${DOCKER_TAG}
# Health check
sleep 10
curl -f http://localhost:808${newColor == 'blue' ? '1' : '2'}/actuator/health
# Switch traffic
nginx -s reload
# Stop old container
docker stop app-${currentColor}
docker rm app-${currentColor}
"""
}
}
}
4. 롤백 메커니즘
stage('Rollback') {
when {
expression { currentBuild.result == 'FAILURE' }
}
steps {
script {
def previousTag = sh(
script: "git describe --abbrev=0 --tags",
returnStdout: true
).trim()
sh """
docker stop spring-boot-app-prod
docker rm spring-boot-app-prod
docker run -d \
--name spring-boot-app-prod \
-p 8080:8080 \
${DOCKER_IMAGE}:${previousTag}
"""
}
}
}
보안 및 자격증명 관리
Jenkins Credentials 사용
stage('Secure Deploy') {
steps {
withCredentials([
usernamePassword(
credentialsId: 'database-credentials',
usernameVariable: 'DB_USER',
passwordVariable: 'DB_PASS'
),
string(
credentialsId: 'api-key',
variable: 'API_KEY'
)
]) {
sh '''
docker run -d \
-e DB_USERNAME=${DB_USER} \
-e DB_PASSWORD=${DB_PASS} \
-e API_KEY=${API_KEY} \
${DOCKER_IMAGE}:${DOCKER_TAG}
'''
}
}
}
모니터링 및 알림
Slack 통합
post {
success {
slackSend(
color: 'good',
message: "빌드 성공: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
color: 'danger',
message: "빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER}\n${env.BUILD_URL}"
)
}
}
이메일 알림
post {
failure {
emailext(
subject: "빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
빌드가 실패했습니다.
Job: ${env.JOB_NAME}
Build Number: ${env.BUILD_NUMBER}
Build URL: ${env.BUILD_URL}
""",
to: 'team@example.com'
)
}
}
성능 최적화 팁
1. Maven 캐싱
stage('Build with Cache') {
steps {
sh '''
mvn -Dmaven.repo.local=.m2/repository \
clean package
'''
}
}
2. Docker 레이어 캐싱
# 의존성 레이어 분리
FROM openjdk:17-jdk-slim as dependencies
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
# 애플리케이션 레이어
FROM dependencies as builder
COPY src ./src
RUN mvn package -DskipTests
FROM openjdk:17-jdk-slim
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
3. 병렬 스테이지 활용
stage('Parallel Analysis') {
parallel {
stage('SonarQube') {
steps { sh 'mvn sonar:sonar' }
}
stage('Dependency Check') {
steps { sh 'mvn dependency-check:check' }
}
stage('OWASP Scan') {
steps { sh 'zap-cli quick-scan http://localhost:8080' }
}
}
}
실전 예제: 완전한 파이프라인
@Library('shared-library') _
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-openjdk-17
command: ['cat']
tty: true
- name: docker
image: docker:latest
command: ['cat']
tty: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
"""
}
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 1, unit: 'HOURS')
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build & Test') {
steps {
container('maven') {
sh 'mvn clean verify'
}
}
}
stage('Quality Gate') {
steps {
container('maven') {
script {
def qualityGate = waitForQualityGate()
if (qualityGate.status != 'OK') {
error "품질 게이트 실패: ${qualityGate.status}"
}
}
}
}
}
stage('Build & Push Image') {
steps {
container('docker') {
script {
def app = docker.build("${DOCKER_IMAGE}:${BUILD_NUMBER}")
docker.withRegistry('https://registry.example.com', 'docker-creds') {
app.push()
app.push('latest')
}
}
}
}
}
stage('Deploy') {
steps {
kubernetesDeploy(
configs: 'k8s/*.yaml',
kubeconfigId: 'kubeconfig'
)
}
}
}
}
트러블슈팅
일반적인 문제와 해결방법
문제: Maven 의존성 다운로드 실패
// settings.xml에 미러 설정
sh 'mvn -s settings.xml clean package'
문제: Docker 빌드 시 권한 오류
# Jenkins 사용자를 docker 그룹에 추가
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins
문제: 메모리 부족
environment {
MAVEN_OPTS = '-Xmx2048m -XX:MaxPermSize=512m'
}
결론
Jenkins와 Spring Boot를 활용한 CI/CD 파이프라인 구축함으로서 이러한 장점이 있다.
- 개발 속도 향상: 자동화된 빌드/테스트/배포
- 품질 보증: 지속적인 코드 품질 검증
- 안정적 배포: Blue-Green, 롤백 전략
- 팀 협업: 표준화된 배포 프로세스
Jenkins 가 간단한 프로젝트에서는 무거워서 사용하기 쉽지 않은 부분이 있지만 연습은 해볼만 한 것 같다. 처음 시작할때는 빌드-테스트-배포 파이프라인으로 시작해서 필요에 따라 조금씩 개선해 나가는 것이 좋을 것 같다.
'DevOps > 모니터링' 카테고리의 다른 글
| [모니터링] - 그라파나 알림 매니저 (Grafana AlertManager) 활용하기 (0) | 2024.02.16 |
|---|---|
| [모니터링] - Node.js로 그라파나에서 DB 데이터 로그 보기 (0) | 2024.02.16 |
| [모니터링] - Grafana + Prometheus + cAdvisor로 컨테이너 상태(리소스) 수집하기 (0) | 2024.02.16 |
| [모니터링] - Grafana Loki로 도커 컨테이너 로그 보기 (0) | 2024.02.15 |
| [모니터링] - 그라파나 + 프로메테우스로 네트워크 로그 수집하기 (0) | 2024.02.15 |