지난 포스팅에서는 Nexus Repository에 업로드된 Node 애플리케이션을 기반으로, 다운로드 → 도커라이징 → Nexus Repository에 푸시하는 파이프라인을 생성하고 테스트를 진행하였습니다.
이번 포스팅에서는 GitHub에 Node 애플리케이션의 태그가 푸시되었을 때, 아래 절차대로 자동화된 빌드가 이루어지도록 작업을 진행해보겠습니다.
해당 작업을 수행하기에 앞서 Node Pipeline과 Docker Pipeline 구성이 선행되어야 합니다.
Node Pipeline - Link
Docker Pipeline - Link

Node Pipeline과 Docker Pipeline을 분리하는 이유는, Node 애플리케이션이 VM 또는 Container 환경에서 실행될 수 있도록 각각의 형태로 별도로 저장하기 위함입니다.
전체 빌드 순서는 다음과 같이 구성됩니다.
GitHub Action
Node Pipeline
해당 태그 기반으로 npm install, ncc build를 진행하고, 결과물을 Nexus Repository에 업로드
빌드가 성공하면 10초 후 Docker Pipeline을 트리거 발생
Docker Pipeline
Nexus에서 업로드된 Node 애플리케이션을 다운로드하여 Dockerizing 수행
도커 이미지를 Nexus Docker Repository에 푸시
위의 작업을 진행하기 위해선 몇가지 사전 작업 진행이 필요합니다.
Pipeline를 원격으로 실행 시키기 위해선 API token 생성이 필요합니다.
생성 절차는 다음과 같습니다.
Jenkins 관리 → Users 클릭

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

Security를 클릭합니다.

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

Token명을 입력한 후에 생성합니다.
생성된 Token를 복사 합니다. (최초 생성시에만 보여지므로 반드시 복사)
Jenkins Job 실행/ 실패 등 다양한 환경에서 작업이 발생하였을때 알람을 발생 시키기 위한 작업입니다.
Slack 워크스페이스에서 앱 디렉토리로 이동합니다
알람을 확인하실 워크스페이스를 선택 후 검색창에 Jenkins CI를 입력하여 앱을 찾습니다.

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

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

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

workspace - 팀 하위 도메인 (slack 설정 지침에서 복사)
credentials - 통합 토큰 자격 증명 ID (slack 설정 지침에서 복사)
Default channel / member id : 알람 발생시킬 채널명

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

Github Action를 사용하기전 몇가지 사항을 Secret를 등록해야 합니다.
JENKINS_API_TOKEN : Jenkins API Token
JENKINS_URL : Jenkins 주소 (http:// ….)
JENKINS_USER : Jenkins 로그인 계정

해당 github Repository 접근
상단에 Settings 탭 클릭

왼쪽 매뉴 → Security → Secrets and variables → Action 클릭

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

사전 작업이 완료되었다면 이제 본격적으로 파이프라인 작업을 진행합니다.
우선 Node 애플리케이션에 워크플로우를 위한 yml 작업 진행이 필요합니다.
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 }}
조건 : 모든 태그가 push 될때마다 실행
on:
push:
tags:
- 'v*'
실행 환경 : GitHub에서 제공하는 Ubuntu 운영체제를 기반으로 한 최신 버전(ubuntu-latest)의 가상 환경(runner)에서 실행
trigger-jenkins:
runs-on: ubuntu-latest
Github에 push 된 테그를 조회 하는 코드입니다.
TAG_NAME=${GITHUB_REF#refs/tags/}
echo "Detected tag: $TAG_NAME"
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')
curl : curl을 통하여 해당 파이프라인을 원격 빌드를 실행한다.
curl -X POST $JENKINS_URL/job/$JOB_NAME/buildWithParameters \
-H "Jenkins-Crumb:$CRUMB" \
--data "TAG_NAME=$TAG_NAME"
env : Secret에 저장 된 parameter
env:
JENKINS_URL: ${{ secrets.JENKINS_URL }}
JENKINS_USER: ${{ secrets.JENKINS_USER }}
JENKINS_API_TOKEN: ${{ secrets.JENKINS_API_TOKEN }}
Github Action 워크플로우 작업이 완료되었다면 원격으로 빌드시킬 Node Pipeline용 Trigger pipeline 작업을 진행합니다.
왼쪽 매뉴 → 새로운 Item 클릭

Job Name 입력, pipeline 선택 후 생성

Trigger → 빌드를 원격으로 유발 체크
Authentication Token : node-pipeline-trigger

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"
)
}
}
}
# 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')
}
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}'
}
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}"
}
}
}
브랜치 / 태그에 따라서 ref 값을 분기 처리
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/*'
]]
])
}
}
}
install : node module install 작업 진행
build : 해당 애플리케이션에 대해 node ncc (프로젝트 경량화) 작업 진행
stage('npm install') {
steps {
sh 'npm install'
}
}
stage('npm run build') {
steps {
sh 'npm run build'
}
}
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') {
steps {
sh "cd dist && npm publish --registry=http://${env.NPM_REGISTRY}/repository/express-winston/"
}
}docker-pipeline-trigger 에 전달docker-pipeline-trigger 실행 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} ${buildUrl}/console"
)
}
failure {
slackSend(
channel: '#jenkins',
color: 'danger',
message: "❌ 빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER} ${buildUrl}/console"
)
}
}Node Pipeline 작업이 완료되었다면 마지막으로 Docker Pipeline용 Trigger pipeline 작업을 진행합니다.
왼쪽 매뉴 → 새로운 Item 클릭

Job Name 입력, pipeline 선택 후 생성

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"
)
}
}
}
node-pipeline-trigger로 부터 TAG 파리미터를 전달parameters {
string(name: 'TAG_NAME', defaultValue: '', description: 'TAG_NAME')
}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'
}
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}"
}
}
}
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
"""
}
}
}
도커 빌드 및 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 {
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"
)
}
}해당 작업이 완료되었다면 이제 테스트를 진행하겠습니다.
v1.0.10 push 진행



GitHub에 태그가 푸시되면 Jenkins가 자동으로 Node 애플리케이션을 빌드하고, 이를 도커 이미지로 변환하여 Nexus에 업로드하는 전체 자동화 파이프라인을 구현하였습니다.
다음 포스팅에서는 Slack과 Azure Function을 연동하여 특정 메세지 입력시 인프라 자동화를 확장하는 내용을 다뤄보겠습니다.