[Jenkins] 3. gitHub 태그 푸시로 시작하는 CI/CD 자동화

bocopile·2025년 4월 12일

Jenkins

목록 보기
3/3

지난 포스팅에서는 Nexus Repository에 업로드된 Node 애플리케이션을 기반으로, 다운로드 → 도커라이징 → Nexus Repository에 푸시하는 파이프라인을 생성하고 테스트를 진행하였습니다.

이번 포스팅에서는 GitHub에 Node 애플리케이션의 태그가 푸시되었을 때, 아래 절차대로 자동화된 빌드가 이루어지도록 작업을 진행해보겠습니다.

해당 작업을 수행하기에 앞서 Node Pipeline과 Docker Pipeline 구성이 선행되어야 합니다.

Node Pipeline - Link

Docker Pipeline - Link

사전 설명

Node Pipeline과 Docker Pipeline을 분리하는 이유는, Node 애플리케이션이 VM 또는 Container 환경에서 실행될 수 있도록 각각의 형태로 별도로 저장하기 위함입니다.

전체 빌드 순서는 다음과 같이 구성됩니다.

  1. GitHub Action

    • 태그가 푸시되면 GitHub Action을 통해 Jenkins Node Pipeline을 원격으로 실행
  2. Node Pipeline

    • 해당 태그 기반으로 npm install, ncc build를 진행하고, 결과물을 Nexus Repository에 업로드

    • 빌드가 성공하면 10초 후 Docker Pipeline을 트리거 발생

  3. Docker Pipeline

    • Nexus에서 업로드된 Node 애플리케이션을 다운로드하여 Dockerizing 수행

    • 도커 이미지를 Nexus Docker Repository에 푸시

사전 작업

위의 작업을 진행하기 위해선 몇가지 사전 작업 진행이 필요합니다.

1) Jenkins API Token

Pipeline를 원격으로 실행 시키기 위해선 API token 생성이 필요합니다.

생성 절차는 다음과 같습니다.

  1. Jenkins 관리 → Users 클릭

  2. API Token를 생성할 계정을 클릭합니다.

  1. Security를 클릭합니다.

  1. API Token에서 Add new Token 버튼을 클릭합니다. (해당 이미지는 Token이 이미 생성된 화면입니다.)

  2. Token명을 입력한 후에 생성합니다.

  3. 생성된 Token를 복사 합니다. (최초 생성시에만 보여지므로 반드시 복사)

2) Slack 세팅

Jenkins Job 실행/ 실패 등 다양한 환경에서 작업이 발생하였을때 알람을 발생 시키기 위한 작업입니다.

  1. Slack 워크스페이스에서 앱 디렉토리로 이동합니다

  2. 알람을 확인하실 워크스페이스를 선택 후 검색창에 Jenkins CI를 입력하여 앱을 찾습니다.

  3. 좌측에 Slack에 추가 를 클릭합니다.

  1. 원하시는 채널을 선택하거나 새로운 채널을 생성 후에 Jenkins CI 통합 앱 추가 버튼을 클릭합니다.

  1. 설정 지침에 따라서 Jenkins에서 작업을 진행하시면 됩니다.

  • Global Slack Setting에서 해당 값에 맞춰 입력을 진행하시면 됩니다.
    • workspace - 팀 하위 도메인 (slack 설정 지침에서 복사)

    • credentials - 통합 토큰 자격 증명 ID (slack 설정 지침에서 복사)

    • Default channel / member id : 알람 발생시킬 채널명

  • Add 버튼을 클릭하여 새로운 Credentials 생성
    • kind : Secret text

    • Secret : 통합 토큰 자격 증명 ID (slack 설정 지침에서 복사)

    • ID: slack-token

3) Github Secrets 등록

Github Action를 사용하기전 몇가지 사항을 Secret를 등록해야 합니다.

작업 대상

  • JENKINS_API_TOKEN : Jenkins API Token

  • JENKINS_URL : Jenkins 주소 (http:// ….)

  • JENKINS_USER : Jenkins 로그인 계정

작업 절차

  1. 해당 github Repository 접근

  2. 상단에 Settings 탭 클릭

  3. 왼쪽 매뉴 → SecuritySecrets and variablesAction 클릭

  4. 하단 Repository secrets → New repository secret 클릭하여 생성 진행

작업 진행

사전 작업이 완료되었다면 이제 본격적으로 파이프라인 작업을 진행합니다.

1) GitHub Action 워크플로우 작성

우선 Node 애플리케이션에 워크플로우를 위한 yml 작업 진행이 필요합니다.

Jenkins-Trigger.yaml 작성

name: Trigger Jenkins on Tag Push

on:
  push:
    tags:
      - 'v*'

jobs:
  trigger-jenkins:
    runs-on: ubuntu-latest

    steps:
      - name: Trigger Jenkins Job with Crumb
        run: |
          
          TAG_NAME=${GITHUB_REF#refs/tags/}
          echo "Detected tag: $TAG_NAME"
          
          CRUMB=$(curl -s --user "$JENKINS_USER:$JENKINS_API_TOKEN" \
            "$JENKINS_URL/crumbIssuer/api/json" \
            | sed -E 's/.*"crumb":"([^"]*)".*/\1/' \
            | tr -d '\n')
          
          curl -X POST $JENKINS_URL/job/$JOB_NAME/buildWithParameters \
          -H "Jenkins-Crumb:$CRUMB" \
          --data "TAG_NAME=$TAG_NAME"
        env:
          JENKINS_URL: ${{ secrets.JENKINS_URL }}
          JENKINS_USER: ${{ secrets.JENKINS_USER }}
          JENKINS_API_TOKEN: ${{ secrets.JENKINS_API_TOKEN }}

상세 설명

  1. 조건 : 모든 태그가 push 될때마다 실행

    on:
      push:
        tags:
          - 'v*'
  1. 실행 환경 : GitHub에서 제공하는 Ubuntu 운영체제를 기반으로 한 최신 버전(ubuntu-latest)의 가상 환경(runner)에서 실행

      trigger-jenkins:
        runs-on: ubuntu-latest
  1. Github에 push 된 테그를 조회 하는 코드입니다.

    TAG_NAME=${GITHUB_REF#refs/tags/}
              echo "Detected tag: $TAG_NAME"
  1. CRUMB : Jenkins에서 CSRF 를 방지하기 위해 인증된 사용자만 통과가 되도록 하기 위한 코드 입니다.

    • 해당 정보를 자세히 보면 --user "$JENKINS_USER:$JENKINS_API_TOKEN" 으로 인증과 관련된 파라미터를 전달
    CRUMB=$(curl -s --user "$JENKINS_USER:$JENKINS_API_TOKEN" \
                "$JENKINS_URL/crumbIssuer/api/json" \
                | sed -E 's/.*"crumb":"([^"]*)".*/\1/' \
                | tr -d '\n')
  2. curl : curl을 통하여 해당 파이프라인을 원격 빌드를 실행한다.

    curl -X POST $JENKINS_URL/job/$JOB_NAME/buildWithParameters \
              -H "Jenkins-Crumb:$CRUMB" \
              --data "TAG_NAME=$TAG_NAME"
  1. env : Secret에 저장 된 parameter

            env:
              JENKINS_URL: ${{ secrets.JENKINS_URL }}
              JENKINS_USER: ${{ secrets.JENKINS_USER }}
              JENKINS_API_TOKEN: ${{ secrets.JENKINS_API_TOKEN }}

2) Jenkins - Node Pipeline 설정

Github Action 워크플로우 작업이 완료되었다면 원격으로 빌드시킬 Node Pipeline용 Trigger pipeline 작업을 진행합니다.

작업 절차

  1. 왼쪽 매뉴 → 새로운 Item 클릭

  1. Job Name 입력, pipeline 선택 후 생성

  1. Trigger → 빌드를 원격으로 유발 체크

    Authentication Token : node-pipeline-trigger

  1. Pipeline Script에 아래 코드를 입력

    pipeline {
        agent any
        parameters {
            string(name: 'TAG_NAME', defaultValue: '', description: 'GitHub pushed tag')
        }
    
        environment {
            GIT_CREDENTIALS = credentials('github_credentials')
            NPM_AUTH_TOKEN = credentials('npm_login_token_cicd')
            NPM_REGISTRY = '${NEXUS_REPO_URL}:8081'
        }
        tools {
            nodejs 'nodejs-18.20.6'
        }
        stages {
            stage('Init Vars') {
                steps {
                    script {
                        // Jenkins 진행중인 Job 로그를 확인하기 위한 목적
                        currentBuild.displayName = "#${env.BUILD_NUMBER}"
                        def jobName = env.JOB_NAME
                        def buildNumber = env.BUILD_NUMBER
                        env.buildUrl = env.BUILD_URL ?: "${env.JENKINS_URL}job/${jobName}/${buildNumber}/"
    
                        // TAG_NAME 파라미터 값 출력
                        echo "TAG_NAME: ${params.TAG_NAME}"
                    }
                }
            }
            stage('git CheckOut') {
                steps {
                    script {
                        // 파라미터 값이 브랜치인지 태그인지 확인
                        def ref = params.TAG_NAME
                        if (ref.startsWith("origin/")) {
                            // 브랜치라면 origin/ 접두사를 제거
                            ref = ref - "origin/"
                        } else {
                            // 태그라면 refs/tags/ 접두사를 붙여 checkout 하도록 변경
                            ref = "refs/tags/" + ref
                        }
                        checkout([
                                $class           : 'GitSCM',
                                branches         : [[name: ref]],
                                userRemoteConfigs: [[
                                                            url          : 'https://github.com/bocopile/express-winston.git',
                                                            credentialsId: 'github_credentials',
                                                            refspec      : '+refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/*'
                                                    ]]
                        ])
                    }
                }
            }
            stage('npm install') {
                steps {
    
                    sh 'npm install'
                }
            }
            stage('npm run build') {
                steps {
                    sh 'npm run build'
                }
            }
            stage('Login to npm Registry') {
                steps {
                    script {
                        sh """
                          echo "Logging in to npm registry..."
                          echo "//${env.NPM_REGISTRY}/repository/express-winston/:_authToken=\${NPM_AUTH_TOKEN}" > ~/workspace/node-pipeline-trigger/dist/.npmrc
                        """
                        sh "npm whoami --registry=http://${env.NPM_REGISTRY}/repository/express-winston/"
                    }
                }
            }
            stage('publish') {
                steps {
                    sh "cd dist && npm publish --registry=http://${env.NPM_REGISTRY}/repository/express-winston/"
                }
            }
            stage('Trigger Docker Pipeline') {
                steps {
                    build job: 'docker-pipeline-trigger',
                            parameters: [
                                    string(name: 'TAG_NAME', value: "${params.TAG_NAME}")
                            ],
                            quietPeriod: 10,  // 10초 대기 후 잡 실행
                            wait: false
                }
            }
        }
        
        post {
            success {
                slackSend(
                        channel: '#jenkins',
                        color: 'good',
                        message: "✅ 빌드 성공: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${env.buildUrl}/console"
                )
            }
            failure {
                slackSend(
                        channel: '#jenkins',
                        color: 'danger',
                        message: "❌ 빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${env.buildUrl}/console"
                )
            }
        }
    }

상세 설명

  • parameter : Tag명을 String parameter로 받음
    # Github Action에서 호출  
    curl -X POST $JENKINS_URL/job/$JOB_NAME/buildWithParameters \
              -H "Jenkins-Crumb:$CRUMB" \
              --data "TAG_NAME=$TAG_NAME"  
        
    # pipeline   
    parameters {
        string(name: 'TAG_NAME', defaultValue: '', description: 'GitHub pushed tag')
    }
    
  • enviroment : 파라미터를 사전에 세팅
    • GIT_CREDENTIALS : GitHub Node 애플리케이션 clone 받기 위한 토근

    • NPM_AUTH_TOKEN : Nexus Repository에 애플리케이션 업로드 하기 위한 토큰

    • NPM_REGISTRY : Nexus Repository URL

        environment {
            GIT_CREDENTIALS = credentials('github_credentials') # Github Token
            NPM_AUTH_TOKEN = credentials('npm_login_token_cicd') # NPM Nexus Token
            NPM_REGISTRY = '${NEXUS_REPO_URL}:${NEXUS_REPO_PORT}'
        }
  • Stages - init Vars
    • slack 알람을 위한 Jenkins Url 전역 파라미터 세팅

    • Tag name 확인

              stage('Init Vars') {
                  steps {
                      script {
                          // Jenkins 진행중인 Job 로그를 확인하기 위한 목적
                          currentBuild.displayName = "#${env.BUILD_NUMBER}"
                          def jobName = env.JOB_NAME
                          def buildNumber = env.BUILD_NUMBER
                          env.buildUrl = env.BUILD_URL ?: "${env.JENKINS_URL}job/${jobName}/${buildNumber}"
      
                          // TAG_NAME 파라미터 값 출력
                          echo "TAG_NAME: ${params.TAG_NAME}"
                      }
                  }
              }
  • Stages - git Checkout
    • 브랜치 / 태그에 따라서 ref 값을 분기 처리

      • 브랜치 : origin/***
      • 태그 : ref/tags/***
    • git checkout 작업 진행

              stage('git CheckOut') {
                  steps {
                      script {
                          // 파라미터 값이 브랜치인지 태그인지 확인
                          def ref = params.TAG_NAME
                          if (ref.startsWith("origin/")) {
                              // 브랜치라면 origin/ 접두사를 제거
                              ref = ref - "origin/"
                          } else {
                              // 태그라면 refs/tags/ 접두사를 붙여 checkout 하도록 변경
                              ref = "refs/tags/" + ref
                          }
                          checkout([
                                  $class           : 'GitSCM',
                                  branches         : [[name: ref]],
                                  userRemoteConfigs: [[
                                                              url          : 'https://github.com/bocopile/express-winston.git',
                                                              credentialsId: 'github_credentials',
                                                              refspec      : '+refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/*'
                                                      ]]
                          ])
                      }
                  }
              }
  • Stages - npm install / build
    • install : node module install 작업 진행

    • build : 해당 애플리케이션에 대해 node ncc (프로젝트 경량화) 작업 진행

              stage('npm install') {
                  steps {
      
                      sh 'npm install'
                  }
              }
              stage('npm run build') {
                  steps {
                      sh 'npm run build'
                  }
              }
  • Stage - Login to npm Registry
    • Nexus Repository 업로드 전에 로그인 및 계정 확인 작업 진행

              stage('Login to npm Registry') {
                  steps {
                      script {
                          sh """
                            echo "Logging in to npm registry..."
                            echo "//${env.NPM_REGISTRY}/repository/express-winston/:_authToken=\${NPM_AUTH_TOKEN}" > ~/workspace/node-pipeline-trigger/.npmrc
                          """
                          sh "npm whoami --registry=http://${env.NPM_REGISTRY}/repository/express-winston/"
                      }
                  }
              }
  • Stage - Publish
    • 빌드가 완료된 Node 애플리케이션을 Nexus Repository에 업로드 진행
              stage('publish') {
                  steps {
                      sh "cd dist && npm publish --registry=http://${env.NPM_REGISTRY}/repository/express-winston/"
                  }
              }
  • Stage- trigger Docker Pipeline
    • Node Pipeline 작업 후에 TAG_NAME 을 docker-pipeline-trigger 에 전달
    • 10초 대기 후에 docker-pipeline-trigger 실행
      • publish 후에 업로드 되기까지의 약간의 시간이 필요
                stage('Trigger Docker Pipeline') {
                    steps {
                        build job: 'docker-pipeline-trigger',
                                parameters: [
                                        string(name: 'TAG_NAME', value: "${params.TAG_NAME}")
                                ],
                                quietPeriod: 10,  // 10초 대기 후 잡 실행
                                wait: false
                    }
                }
  • post - Jenkins Job 성공/실패를 slack에 발송
        post {
            success {
                slackSend(
                        channel: '#jenkins',
                        color: 'good',
                        message: "✅ 빌드 성공: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${buildUrl}/console"
                )
            }
            failure {
                slackSend(
                        channel: '#jenkins',
                        color: 'danger',
                        message: "❌ 빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${buildUrl}/console"
                )
            }
        }

3) Jenkins - Docker Pipeline 설정

Node Pipeline 작업이 완료되었다면 마지막으로 Docker Pipeline용 Trigger pipeline 작업을 진행합니다.

작업 절차

  1. 왼쪽 매뉴 → 새로운 Item 클릭

  1. Job Name 입력, pipeline 선택 후 생성

  1. pipeline script에 해당 코드 작성

    pipeline {
        agent any
        parameters {
            string(name: 'TAG_NAME', defaultValue: '', description: 'TAG_NAME')
        }
        environment {
            DOCKER_IMAGE = 'express-winston'
            DOCKER_REGISTRY = 'http://${DOCKER_REGISTRY_URL}:5000'
            NPM_REGISTRY = '${NPM_REGISTRY}:8081'
        }
        stages {
            stage('Init Vars') {
                steps {
                    script {
                        // 전역 공유를 위한 전역 변수 등록 (global-ish 방식)
                        currentBuild.displayName = "#${env.BUILD_NUMBER}"
                        def jobName = env.JOB_NAME
                        def buildNumber = env.BUILD_NUMBER
                        env.JOB_URL = env.BUILD_URL ?: "${env.JENKINS_URL}job/${jobName}/${buildNumber}/"
                        env.VERSION = params.TAG_NAME.replaceFirst('v', '')
                        echo "TAG_NAME: ${params.TAG_NAME}"
                        echo "VERSION: ${env.VERSION}"
                    }
                }
            }
            stage('Download & Unzip') {
                steps {
                    withCredentials([usernamePassword(
                            credentialsId: 'nexus-registry-credentials',
                            usernameVariable: 'NEXUS_ID',
                            passwordVariable: 'NEXUS_PASSWORD'
                    )]) {
                        script {
                            sh """
                                rm -rf package *.tgz
                                wget --user=${NEXUS_ID} --password='${NEXUS_PASSWORD}' http://${NPM_REGISTRY}/repository/express-winston/express-winston/-/express-winston-${env.VERSION}.tgz
                                tar -xvzf express-winston-${env.VERSION}.tgz 
                            """
                        }
                    }
                }
            }
    
            stage('Check Version & Docker Push') {
                steps {
                    script {
                        def pkgVersion = sh(
                                script: "jq -r '.version' package/package.json",
                                returnStdout: true
                        ).trim()
    
                        echo "📦 package.json 버전: ${pkgVersion}"
    
                        docker.withRegistry(DOCKER_REGISTRY, 'nexus-registry-credentials') {
                            def image = docker.build("${DOCKER_IMAGE}:${pkgVersion}", "package")
                            image.push()
                            image.push('latest')
                        }
                    }
                }
            }
        }
    
        post {
            success {
                slackSend (
                        channel: '#jenkins',
                        color: 'good',
                        message: "✅ 빌드 성공: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${env.JOB_URL}/console"
                )
            }
            failure {
                slackSend (
                        channel: '#jenkins',
                        color: 'danger',
                        message: "❌ 빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${env.JOB_URL}/console"
                )
            }
        }
    }
    

상세 설명

  • parameter : node-pipeline-trigger로 부터 TAG 파리미터를 전달
    parameters {
    	string(name: 'TAG_NAME', defaultValue: '', description: 'TAG_NAME')
    }
  • environment : 파라미터를 사전에 세팅
    • DOCKER_IMAGE : 도커 이미지명

    • DOCKER_REGISTRY : 도커 레지스트리 URL

    • NPM_REGISTRY : NPM 레지스트리 URL

      environment {
      	DOCKER_IMAGE = 'express-winston'
      	DOCKER_REGISTRY = 'http://${DOCKER_REGISTRY_URL}:5000'
        NPM_REGISTRY = '${NPM_REGISTRY}:8081'
      }
  • stage - init var
    • slack 알람을 위한 Jenkins Url 전역 파라미터 세팅

    • tag 형식이 v1.x.x 으로 오는데 1.x.x 로 치환 작업 진행

              stage('Initialize') {
                  steps {
                      script {
                          // 빌드 번호와 URL 등의 변수를 한 곳에서 설정
                          currentBuild.displayName = "#${env.BUILD_NUMBER}"
                          def jobName = env.JOB_NAME
                          def buildNumber = env.BUILD_NUMBER
                          env.JOB_URL = env.BUILD_URL ?: "${env.JENKINS_URL}job/${jobName}/${buildNumber}/"
                          // TAG_NAME에서 접두사 v 제거 후 VERSION 저장 (전역 변수 사용 시 env 변수 활용)
                          env.VERSION = params.TAG_NAME.replaceFirst('v', '')
                          echo "VERSION: ${env.VERSION}"
                      }
                  }
              }
  • stage - Download & Unzip
    • Nexus에서 .tgz 패키지를 다운로드하여 압축 해제

              stage('Download & Unzip') {
                  steps {
                      withCredentials([usernamePassword(
                              credentialsId: 'nexus-registry-credentials',
                              usernameVariable: 'NEXUS_ID',
                              passwordVariable: 'NEXUS_PASSWORD'
                      )]) {
                          // 스크립트 내부에서 여러 명령을 한 번에 실행
                          sh """
                              rm -rf package *.tgz
                              wget --user=\${NEXUS_ID} --password=\${NEXUS_PASSWORD} http://\${NPM_REGISTRY}/repository/express-winston/express-winston/-/express-winston-\${env.VERSION}.tgz
                              tar -xvzf express-winston-\${env.VERSION}.tgz 
                          """
                      }
                  }
              }
  • stage - Docker Build & Push
    - jq를 통해 package.json의 버전 정보 추출
    • 도커 빌드 및 push (버전 태그, latest 태그)

              stage('Docker Build & Push') {
                  steps {
                      script {
                          def pkgVersion = sh(
                                  script: "jq -r '.version' package/package.json",
                                  returnStdout: true
                          ).trim()
                          echo "📦 package.json 버전: ${pkgVersion}"
                          docker.withRegistry(DOCKER_REGISTRY, 'nexus-registry-credentials') {
                              def image = docker.build("${DOCKER_IMAGE}:${pkgVersion}", "package")
                              image.push()
                              image.push('latest')
                          }
                      }
                  }
              }
  • post - Jenkins Job 성공/실패를 slack에 발송
        post {
            success {
                slackSend(
                        channel: '#jenkins',
                        color: 'good',
                        message: "✅ 빌드 성공: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${env.JOB_URL}/console"
                )
            }
            failure {
                slackSend(
                        channel: '#jenkins',
                        color: 'danger',
                        message: "❌ 빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${env.JOB_URL}/console"
                )
            }
        }

테스트 진행

해당 작업이 완료되었다면 이제 테스트를 진행하겠습니다.

  • Tag v1.0.10 push 진행
  • Github Action 로그 확인
  • Node Pipeline 확인
  • Nexus Node Repository 확인 - express-winston-1.0.10.tgz 업로드 확인
  • Docker Pipeline 확인
  • Nexus Docker Repository 확인 - 1.0.10버전 업로드 확인

정리

GitHub에 태그가 푸시되면 Jenkins가 자동으로 Node 애플리케이션을 빌드하고, 이를 도커 이미지로 변환하여 Nexus에 업로드하는 전체 자동화 파이프라인을 구현하였습니다.

다음 포스팅에서는 Slack과 Azure Function을 연동하여 특정 메세지 입력시 인프라 자동화를 확장하는 내용을 다뤄보겠습니다.

profile
DevOps Engineer

0개의 댓글