현대 오토에버 클라우드 스쿨

개인 프로젝트) 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
반응형