현대 오토에버 클라우드 스쿨
개인 프로젝트) Jenkins로 CICD 구현
Gom3rye
2025. 10. 20. 14:08
728x90
반응형
1. 파이프라인 시작
pipeline {
agent {
kubernetes {
namespace 'jenkins'
yaml '''
apiVersion: v1
kind: Pod
spec:
serviceAccountName: jenkins-agent
restartPolicy: Never
volumes:
- name: gradle-cache
persistentVolumeClaim:
claimName: gradle-cache-pvc
- name: kaniko-cache
persistentVolumeClaim:
claimName: kaniko-cache-pvc
- name: docker-config
secret:
secretName: dockerhub-secret
items:
- key: .dockerconfigjson
path: config.json
- name: workspace-volume
emptyDir: {}
containers:
- name: gradle
image: gradle:8.5.0-jdk21
command: ["sleep"]
args: ["infinity"]
volumeMounts:
- name: gradle-cache
mountPath: /home/jenkins/.gradle
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: kaniko-command
image: gcr.io/kaniko-project/executor:v1.23.2-debug
command: ["/busybox/cat"]
tty: true
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker
- name: kaniko-cache
mountPath: /cache
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: kaniko-query
image: gcr.io/kaniko-project/executor:v1.23.2-debug
command: ["/busybox/cat"]
tty: true
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker
- name: kaniko-cache
mountPath: /cache
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: kaniko-frontend
image: gcr.io/kaniko-project/executor:v1.23.2-debug
command: ["/busybox/cat"]
tty: true
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker
- name: kaniko-cache
mountPath: /cache
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: kubectl
image: dtzar/helm-kubectl:3.15.0
command: ["sleep"]
args: ["infinity"]
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
'''
}
}
- agent kubernetes: Jenkins가 쿠버네티스에서 파드를 띄워 작업을 수행한다는 선언.
- namespace 'jenkins': 작업할 네임스페이스를 지정.
- yaml: 쿠버네티스 Pod 정의를 직접 선언.
- serviceAccountName: jenkins-agent: Pod에서 사용할 서비스 계정.
- volumes:
- gradle-cache: Gradle 빌드 캐시를 위한 PVC(영구 저장소).
- kaniko-cache: Kaniko (도커 이미지 빌드 툴) 캐시용 PVC.
- docker-config: 도커 허브 로그인 정보가 담긴 시크릿 마운트.
- workspace-volume: 여러 컨테이너가 공유할 작업 공간(빈 디렉터리).
- containers:
- gradle: Gradle 빌드용, 무한 대기 상태로 대기(sleep infinity)하여 명령 대기.
- kaniko-command, kaniko-query, kaniko-frontend: Kaniko executor 이미지 3개(각각 command-service, query-service, todo-frontend용) - 빌드에 독립적 사용.
- kubectl: 쿠버네티스와 헬름 명령어 실행용 컨테이너.
2. 옵션 설정
options {
timeout(time: 45, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10'))
}
- 파이프라인 실행 최대 시간 45분 제한.
- 최근 10개 빌드 기록만 보관하고 나머지는 삭제.
3. 환경 변수
environment {
DOCKERHUB_REPO = 'kyla333'
DEPLOY_NAMESPACE = 'prod'
}
- DOCKERHUB_REPO: 도커 허브 사용자명 혹은 리포지토리명.
- DEPLOY_NAMESPACE: 쿠버네티스 배포 대상 네임스페이스(production).
4. Stages
Stage: Initialize
stage('Initialize') {
steps {
script {
env.IMAGE_TAG = sh(
script: 'git rev-parse --short=7 HEAD',
returnStdout: true
).trim()
echo "📦 IMAGE_TAG: ${env.IMAGE_TAG}"
}
}
}
- git rev-parse --short=7 HEAD: 현재 커밋의 7자리 해시를 가져와 IMAGE_TAG 환경 변수에 저장.
- 이후 도커 이미지 태그로 활용.
Stage: Detect Changes
stage('Detect Changes') {
steps {
script {
env.CHANGED_SERVICES = detectChangedServices()
if (!env.CHANGED_SERVICES) {
echo "ℹ️ No service code changes - skipping build"
currentBuild.result = 'SUCCESS'
currentBuild.description = "No changes"
env.SKIP_BUILD = 'true'
} else {
echo "🔍 Changed: ${env.CHANGED_SERVICES}"
currentBuild.description = "Building: ${env.CHANGED_SERVICES}"
}
}
}
}
- 사용자 정의 함수 detectChangedServices() 호출해서 변경된 서비스 목록을 판단.
- 변경 없으면 빌드 스킵 플래그(SKIP_BUILD) 설정.
- 변경 있으면 어떤 서비스가 바뀌었는지 출력 및 빌드 설명 설정.
Stage: Build Backend Services
stage('Build Backend Services') {
when {
expression {
env.SKIP_BUILD != 'true' &&
(env.CHANGED_SERVICES.contains('command-service') ||
env.CHANGED_SERVICES.contains('query-service'))
}
}
steps {
container('gradle') {
script {
def services = env.CHANGED_SERVICES.split(',')
services.each { svc ->
if (svc in ['command-service', 'query-service']) {
echo "🔨 Building ${svc}..."
sh """
cd ${svc}
chmod +x gradlew
./gradlew clean bootJar -x test --no-daemon
echo "✅ ${svc} build completed"
ls -lh build/libs/
"""
}
}
}
}
}
}
- 조건:
- 빌드 스킵 플래그가 없고,
- 변경된 서비스에 백엔드(command-service, query-service)가 포함된 경우.
- gradle 컨테이너에서 변경된 각 백엔드 서비스별로 Gradle 빌드 실행(bootJar 생성, 테스트 제외).
- 빌드 결과 확인 및 출력.
Stage: Build & Push Images
stage('Build & Push Images') {
when {
expression { env.SKIP_BUILD != 'true' }
}
steps {
script {
def services = env.CHANGED_SERVICES.split(',')
def imageTag = env.IMAGE_TAG
def dockerRepo = env.DOCKERHUB_REPO
def builds = [:]
if (services.contains('command-service')) {
builds['Command Service'] = {
container('kaniko-command') {
sh """
/kaniko/executor \
--context=\${WORKSPACE}/command-service \
--dockerfile=\${WORKSPACE}/command-service/Dockerfile \
--destination=${dockerRepo}/command-service:${imageTag} \
--destination=${dockerRepo}/command-service:latest \
--cache=true \
--cache-ttl=24h \
--cache-dir=/cache \
--skip-unused-stages \
--compressed-caching=false
"""
echo "✅ command-service:${imageTag} pushed"
}
}
}
if (services.contains('query-service')) {
builds['Query Service'] = {
container('kaniko-query') {
sh """
/kaniko/executor \
--context=\${WORKSPACE}/query-service \
--dockerfile=\${WORKSPACE}/query-service/Dockerfile \
--destination=${dockerRepo}/query-service:${imageTag} \
--destination=${dockerRepo}/query-service:latest \
--cache=true \
--cache-ttl=24h \
--cache-dir=/cache \
--skip-unused-stages \
--compressed-caching=false
"""
echo "✅ query-service:${imageTag} pushed"
}
}
}
if (services.contains('todo-frontend')) {
builds['Frontend'] = {
container('kaniko-frontend') {
sh """
/kaniko/executor \
--context=\${WORKSPACE}/todo-frontend \
--dockerfile=\${WORKSPACE}/todo-frontend/Dockerfile \
--destination=${dockerRepo}/todo-frontend:${imageTag} \
--destination=${dockerRepo}/todo-frontend:latest \
--cache=true \
--cache-ttl=24h \
--cache-dir=/cache \
--skip-unused-stages \
--compressed-caching=false
"""
echo "✅ todo-frontend:${imageTag} pushed"
}
}
}
parallel builds
}
}
}
- 빌드 스킵 안했을 때 실행.
- 변경된 각 서비스별로 kaniko 컨테이너를 이용해 도커 이미지를 빌드 & 푸시.
- kaniko는 도커 데몬 없이 이미지 빌드하는 도구.
- parallel builds로 병렬 처리해 빠른 빌드 가능.
- 캐시, latest 태그 함께 관리.
- 이미지 태그는 git 커밋 해시 기반.
Stage: Deploy to Production
stage('Deploy to Production') {
when {
expression { env.SKIP_BUILD != 'true' }
}
steps {
container('kubectl') {
script {
def services = env.CHANGED_SERVICES.split(',')
def imageTag = env.IMAGE_TAG
def dockerRepo = env.DOCKERHUB_REPO
def namespace = env.DEPLOY_NAMESPACE
echo "🚀 Deploying to ${namespace}: ${services.join(', ')}"
services.each { svc ->
def deploymentName = (svc == 'todo-frontend') ? 'frontend-deployment' : "${svc}-deployment"
def containerName = (svc == 'todo-frontend') ? 'frontend' : svc
echo "📦 Updating ${deploymentName}..."
sh """
kubectl set image deployment/${deploymentName} \
${containerName}=${dockerRepo}/${svc}:${imageTag} \
-n ${namespace}
echo "⏳ Waiting for rollout..."
kubectl rollout status deployment/${deploymentName} \
-n ${namespace} \
--timeout=5m
echo "✅ ${deploymentName} deployed"
"""
}
}
}
}
}
- 빌드 스킵 안했으면 실행.
- kubectl 컨테이너에서 변경된 서비스들의 쿠버네티스 Deployment에 이미지 태그 업데이트 및 롤아웃 대기.
- todo-frontend 서비스는 배포명과 컨테이너명이 별도로 설정되어 있음.
- 배포 성공 여부를 확인 후 다음으로 진행.
5. Post 빌드 작업
post {
success {
script {
if (env.SKIP_BUILD == 'true') {
echo "✅ No changes - pipeline skipped"
} else {
echo """
✅ CI/CD Pipeline Completed!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 Image Tag: ${env.IMAGE_TAG}
🚀 Deployed: ${env.CHANGED_SERVICES}
🌐 Namespace: ${env.DEPLOY_NAMESPACE}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
}
}
}
failure {
echo """
❌ Pipeline Failed
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Check logs above for details.
Commit: ${env.GIT_COMMIT}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
}
always {
echo "🏁 Finished at ${new Date()}"
}
}
- 성공 시:
- 빌드 스킵 시 별도 메시지 출력
- 아니면 배포 정보 출력
- 실패 시: 에러 메시지와 커밋 정보 출력.
- 항상: 완료 시간 출력.
6. 사용자 정의 함수: detectChangedServices()
def detectChangedServices() {
if (!env.GIT_PREVIOUS_SUCCESSFUL_COMMIT) {
echo "🆕 First build - deploying all services"
return 'command-service,query-service,todo-frontend'
}
def diff = sh(
script: "git diff --name-only ${env.GIT_PREVIOUS_SUCCESSFUL_COMMIT}..${env.GIT_COMMIT}",
returnStdout: true
).trim()
if (!diff) {
return ''
}
def changedFiles = diff.split('\n').findAll { it?.trim() }
def services = ['command-service', 'query-service', 'todo-frontend']
def changedServices = []
services.each { svc ->
if (changedFiles.any { it.startsWith("${svc}/") }) {
changedServices << svc
}
}
if (changedServices.isEmpty() && changedFiles.any { it == 'Jenkinsfile' }) {
echo "⚙️ Only Jenkinsfile changed - skipping"
return ''
}
return changedServices.join(',')
}
- 첫 빌드이면 무조건 모든 서비스 배포.
- 이전 성공 커밋과 현재 커밋간 차이를 git diff --name-only로 구함.
- 변경된 파일 경로에 따라 어떤 서비스가 바뀌었는지 판단.
- command-service, query-service, todo-frontend 3개 서비스 기준.
- 만약 서비스 소스 코드는 바뀌지 않고 Jenkinsfile만 바뀌었다면 빌드 스킵.
- 변경된 서비스 리스트를 쉼표로 연결해 반환.
요약
- 쿠버네티스 기반 Jenkins 파이프라인으로 여러 컨테이너에서 병렬 빌드 및 이미지 푸시 수행.
- Git 변경 사항 기반 빌드 & 배포 결정.
- Gradle 빌드 → Kaniko 이미지 빌드 & 푸시 → kubectl을 이용한 쿠버네티스 배포.
- 캐시, 병렬 처리, 자동 배포 상태 확인까지 포함된 효율적인 CI/CD 파이프라인.
728x90
반응형